"添加前端模板和运行代码模块"
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -10,6 +10,9 @@
|
|||||||
!/qiming-dev-inject/
|
!/qiming-dev-inject/
|
||||||
!/qiming-mcp-proxy/
|
!/qiming-mcp-proxy/
|
||||||
!/qiming-noVNC/
|
!/qiming-noVNC/
|
||||||
|
!/qiming-run_code_rmcp/
|
||||||
|
!/qiming-vite-plugin-design-mode/
|
||||||
|
!/qiming-xagi-frontend-templates/
|
||||||
!/qimingclaw/
|
!/qimingclaw/
|
||||||
!/qimingcode/
|
!/qimingcode/
|
||||||
|
|
||||||
|
|||||||
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
137
qiming-vite-plugin-design-mode/.gitignore
vendored
Normal file
137
qiming-vite-plugin-design-mode/.gitignore
vendored
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
# Logs
|
||||||
|
logs
|
||||||
|
*.log
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
lerna-debug.log*
|
||||||
|
|
||||||
|
# Diagnostic reports (https://nodejs.org/api/report.html)
|
||||||
|
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
|
||||||
|
|
||||||
|
# Runtime data
|
||||||
|
pids
|
||||||
|
*.pid
|
||||||
|
*.seed
|
||||||
|
*.pid.lock
|
||||||
|
|
||||||
|
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||||
|
lib-cov
|
||||||
|
|
||||||
|
# Coverage directory used by tools like istanbul
|
||||||
|
coverage
|
||||||
|
*.lcov
|
||||||
|
|
||||||
|
# nyc test coverage
|
||||||
|
.nyc_output
|
||||||
|
|
||||||
|
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
|
||||||
|
.grunt
|
||||||
|
|
||||||
|
# Bower dependency directory (https://bower.io/)
|
||||||
|
bower_components
|
||||||
|
|
||||||
|
# node-waf configuration
|
||||||
|
.lock-wscript
|
||||||
|
|
||||||
|
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||||
|
build/Release
|
||||||
|
|
||||||
|
# Dependency directories
|
||||||
|
node_modules/
|
||||||
|
jspm_packages/
|
||||||
|
|
||||||
|
# TypeScript v1 declaration files
|
||||||
|
typings/
|
||||||
|
|
||||||
|
# TypeScript cache
|
||||||
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Optional npm cache directory
|
||||||
|
.npm
|
||||||
|
|
||||||
|
# Optional eslint cache
|
||||||
|
.eslintcache
|
||||||
|
|
||||||
|
# Microbundle cache
|
||||||
|
.rpt2_cache/
|
||||||
|
.rts2_cache_cjs/
|
||||||
|
.rts2_cache_es/
|
||||||
|
.rts2_cache_umd/
|
||||||
|
|
||||||
|
# Optional REPL history
|
||||||
|
.node_repl_history
|
||||||
|
|
||||||
|
# Output of 'npm pack'
|
||||||
|
*.tgz
|
||||||
|
|
||||||
|
# Yarn Integrity file
|
||||||
|
.yarn-integrity
|
||||||
|
|
||||||
|
# dotenv environment variables file
|
||||||
|
.env
|
||||||
|
.env.test
|
||||||
|
.env.production
|
||||||
|
|
||||||
|
# parcel-bundler cache (https://parceljs.org/)
|
||||||
|
.cache
|
||||||
|
.parcel-cache
|
||||||
|
|
||||||
|
# Next.js build output
|
||||||
|
.next
|
||||||
|
out
|
||||||
|
|
||||||
|
# Nuxt.js build / generate output
|
||||||
|
.nuxt
|
||||||
|
dist
|
||||||
|
|
||||||
|
# Gatsby files
|
||||||
|
.cache/
|
||||||
|
# Comment in the public line in if your project uses Gatsby and not Next.js
|
||||||
|
# https://nextjs.org/blog/next-9-1#public-directory-support
|
||||||
|
# public
|
||||||
|
|
||||||
|
# vuepress build output
|
||||||
|
.vuepress/dist
|
||||||
|
|
||||||
|
# Serverless directories
|
||||||
|
.serverless/
|
||||||
|
|
||||||
|
# FuseBox cache
|
||||||
|
.fusebox/
|
||||||
|
|
||||||
|
# DynamoDB Local files
|
||||||
|
.dynamodb/
|
||||||
|
|
||||||
|
# TernJS port file
|
||||||
|
.tern-port
|
||||||
|
|
||||||
|
# Stores VSCode versions used for testing VSCode extensions
|
||||||
|
.vscode-test
|
||||||
|
|
||||||
|
# yarn v2
|
||||||
|
.yarn/cache
|
||||||
|
.yarn/unplugged
|
||||||
|
.yarn/build-state.yml
|
||||||
|
.yarn/install-state.gz
|
||||||
|
.pnp.*
|
||||||
|
|
||||||
|
# IntelliJ based IDEs
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
*.iws
|
||||||
|
|
||||||
|
# VS Code
|
||||||
|
.vscode/*
|
||||||
|
!.vscode/extensions.json
|
||||||
|
!.vscode/launch.json
|
||||||
|
!.vscode/settings.json
|
||||||
|
!.vscode/tasks.json
|
||||||
|
|
||||||
|
# macOS
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Local History
|
||||||
|
.history
|
||||||
|
.npm-cache
|
||||||
38
qiming-vite-plugin-design-mode/.npmignore
Normal file
38
qiming-vite-plugin-design-mode/.npmignore
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
# 排除所有 source map 文件(对用户不必要,减少包体积)
|
||||||
|
dist/**/*.map
|
||||||
|
dist/**/*.js.map
|
||||||
|
dist/**/*.d.ts.map
|
||||||
|
*.map
|
||||||
|
|
||||||
|
# 排除 TypeScript 源文件(只保留编译后的文件)
|
||||||
|
**/*.ts
|
||||||
|
**/*.tsx
|
||||||
|
!dist/**/*.d.ts
|
||||||
|
|
||||||
|
# 排除测试文件
|
||||||
|
test/
|
||||||
|
**/*.test.ts
|
||||||
|
**/*.test.tsx
|
||||||
|
**/*.spec.ts
|
||||||
|
**/*.spec.tsx
|
||||||
|
|
||||||
|
# 排除开发文件
|
||||||
|
src/
|
||||||
|
examples/
|
||||||
|
scripts/
|
||||||
|
*.config.ts
|
||||||
|
*.config.js
|
||||||
|
tsconfig*.json
|
||||||
|
vitest.config.ts
|
||||||
|
|
||||||
|
# 排除其他不必要的文件
|
||||||
|
.git/
|
||||||
|
.gitignore
|
||||||
|
.npmignore
|
||||||
|
*.log
|
||||||
|
.DS_Store
|
||||||
|
node_modules/
|
||||||
|
coverage/
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
67
qiming-vite-plugin-design-mode/CHANGELOG.md
Normal file
67
qiming-vite-plugin-design-mode/CHANGELOG.md
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
# Changelog
|
||||||
|
|
||||||
|
All notable changes to `@xagi/vite-plugin-design-mode` are documented in this file.
|
||||||
|
|
||||||
|
## [1.1.0-beta.5] - 2026-04-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Replaced published package workspace protocol dependencies with explicit semver versions to avoid `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND` in external installs.
|
||||||
|
- Aligned internal package dependency graph for published artifacts:
|
||||||
|
- `@xagi/vite-plugin-design-mode -> @xagi/design-mode-client-react/@xagi/design-mode-client-vue/@xagi/design-mode-shared`
|
||||||
|
- `@xagi/design-mode-client-react -> @xagi/design-mode-shared`
|
||||||
|
- `@xagi/design-mode-client-vue -> @xagi/design-mode-shared`
|
||||||
|
|
||||||
|
## [1.1.0-beta.4] - 2026-04-22
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Plugin runtime entry resolution now prefers installed client packages (`@xagi/design-mode-client-react` / `@xagi/design-mode-client-vue`) to avoid publish-time client loading failures.
|
||||||
|
- Hardened file path validation in update middleware and code updater to prevent path-prefix traversal bypass.
|
||||||
|
- Updated and stabilized test suites after monorepo migration; full test run passes.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Published `1.1.0-beta.4` to `next` tag for prerelease consumption.
|
||||||
|
|
||||||
|
## [1.1.0-beta.3] - 2026-04-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Migrated repository to pnpm workspace multi-package layout under `packages/*`.
|
||||||
|
- Removed legacy root `src` implementation and moved active sources to package-local directories.
|
||||||
|
- Updated examples/tests to consume package-based paths.
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Kept CLI support in the plugin package and included CLI entry in published artifacts.
|
||||||
|
|
||||||
|
## [1.1.0-beta.2] - 2026-04-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Dynamic framework support for both `react-vite` and `vue3-vite` projects.
|
||||||
|
- Vue 3 peer dependency declaration (`vue: ^3.0.0`, optional peer).
|
||||||
|
- Release tag strategy documentation (`latest` stable only, `beta/next` for prerelease).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- `framework: 'auto'` behavior is documented as React/Vue 3 auto-detection.
|
||||||
|
- CLI install flow now accepts Vue 3 projects (not only React projects).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Vue 2 guard: plugin now fails fast when detecting `vue@2` or `@vitejs/plugin-vue2`.
|
||||||
|
- npm release workflow now defaults to beta publishing via `npm run release`.
|
||||||
|
|
||||||
|
## [1.1.0-beta.1] - 2026-04-22
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Initial beta channel release for the Vue SFC support line.
|
||||||
|
|
||||||
|
## [1.0.37] - 2026-04-22
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Translated source comments and CLI/user-facing strings to English in core plugin scope.
|
||||||
|
|
||||||
|
## [1.0.36] - 2026-04-10
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- Batch update endpoints and optional history/backup controls.
|
||||||
|
- Validation endpoint and enhanced source metadata return payload.
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Default source attribute prefix updated to `data-xagi`.
|
||||||
|
|
||||||
84
qiming-vite-plugin-design-mode/FEATURES.md
Normal file
84
qiming-vite-plugin-design-mode/FEATURES.md
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
# Vite Design Mode 插件 - 功能文档
|
||||||
|
|
||||||
|
本文档列出了 `@xagi/vite-plugin-design-mode` 当前支持的功能,作为维护、测试和开发的参考。
|
||||||
|
|
||||||
|
## 1. 核心功能 (Core Functionality)
|
||||||
|
|
||||||
|
### 1.0 框架支持与识别 (Framework Support & Detection)
|
||||||
|
- **动态双栈支持**: 支持 `react-vite` 与 `vue3-vite` 两种项目形态。
|
||||||
|
- **自动识别**: `framework: 'auto'` 会根据项目依赖自动选择 React 或 Vue 3 处理链路。
|
||||||
|
- **显式模式**: 可通过 `framework: 'react' | 'vue'` 强制指定处理模式。
|
||||||
|
- **Vue 2 保护**: 检测到 `vue@2` 或 `@vitejs/plugin-vue2` 时,插件会快速失败并提示升级到 Vue 3,避免部分可用的隐性问题。
|
||||||
|
|
||||||
|
### 1.1 元素选择 (Element Selection)
|
||||||
|
- **点击选择**: 点击 iframe 中的元素即可选中。
|
||||||
|
- **高亮显示**: 选中的元素会显示蓝色实线边框 (`outline: 2px solid #007acc`)。
|
||||||
|
- **悬停效果**: 鼠标悬停在元素上时显示虚线边框 (`outline: 1px dashed #007acc`)。
|
||||||
|
- **面包屑导航**: 显示选中元素的 DOM 层级路径。
|
||||||
|
|
||||||
|
### 1.2 组件识别 (Component Identification)
|
||||||
|
- **基本信息**: 显示标签名 (`tagName`)、类名 (`className`) 和文本内容。
|
||||||
|
- **源码映射**: 识别源文件路径 (`fileName`)、行号 (`lineNumber`) 和列号 (`columnNumber`)。
|
||||||
|
- **组件名称**: 识别 React 组件名称(例如 `Button`, `CardTitle`)。
|
||||||
|
- **组件定义**: 显示组件的导入路径(例如 `@/components/ui/button`),以区分组件的使用位置和定义位置。
|
||||||
|
- **父级组件**: 识别 React 树中的父级组件。
|
||||||
|
|
||||||
|
## 2. 编辑能力 (Editing Capabilities)
|
||||||
|
|
||||||
|
### 2.1 静态内容编辑 (Static Content Editing)
|
||||||
|
- **双击编辑**: 双击静态文本元素即可进入 `contentEditable` 模式。
|
||||||
|
- **仅限纯文本**: 只有包含**纯静态文本**(无变量、表达式或子元素)的元素才能编辑。这是通过 `data-xagi-static-content` 属性强制执行的。
|
||||||
|
- **实时预览**: 修改内容会立即反映在浏览器中。
|
||||||
|
- **源码同步**: 失去焦点(保存)时,更改会发送到服务器并写入源文件。
|
||||||
|
|
||||||
|
### 2.2 样式 (Class) 编辑 (Style Editing)
|
||||||
|
- **Class 修改**: 支持添加、删除或修改 Tailwind CSS 类名。
|
||||||
|
- **实时更新**: 类名更改会立即应用到 DOM。
|
||||||
|
- **源码同步**: 更改会持久化保存到源代码中。
|
||||||
|
|
||||||
|
### 2.3 列表项同步 (List Item Synchronization)
|
||||||
|
- **智能分组**: 编辑列表项(例如在 `.map()` 循环中)时,插件会使用生成的 `element-id` 识别所有相关项。
|
||||||
|
- **同步更新**:
|
||||||
|
- **内容**: 编辑一个列表项的静态文本会更新预览中的*所有*实例(以保持一致性)。
|
||||||
|
- **样式**: 修改一个列表项的 Class 会将新样式应用到*所有*实例。
|
||||||
|
|
||||||
|
## 3. 技术实现 (Technical Implementation)
|
||||||
|
|
||||||
|
### 3.1 优化的数据属性 (Optimized Data Attributes)
|
||||||
|
为了减少 DOM 体积,插件采用了紧凑的属性策略:
|
||||||
|
- **`data-xagi-info`**: 单个 JSON 属性,包含所有元数据(`fileName`, `line`, `column`, `componentName`, `importPath`, `elementId`)。
|
||||||
|
- **`data-xagi-element-id`**: 简短且唯一的元素标识符(格式:`file:line:col_tag`)。
|
||||||
|
- **`data-xagi-static-content`**: 布尔标志 (`"true"`),表示元素仅包含纯静态文本。
|
||||||
|
- **`data-xagi-children-source`**: 追踪静态文本子元素的来源位置(用于透传组件)。
|
||||||
|
|
||||||
|
### 3.2 Babel 插件 (`sourceMapper.ts`)
|
||||||
|
- **AST 遍历**: 分析代码以查找 JSX 元素。
|
||||||
|
- **导入追踪**: 追踪 `ImportDeclaration` 以解析组件定义。
|
||||||
|
- **静态分析**: 确定内容是否为静态 (`isStaticContent`)。
|
||||||
|
- **属性注入**: 在构建/服务过程中注入 `data-xagi-*` 属性。
|
||||||
|
|
||||||
|
### 3.2.1 Vue SFC 转换链路 (`vueSfcTransformer.ts`)
|
||||||
|
- **模板解析**: 使用 `@vue/compiler-sfc` 与 `@vue/compiler-dom` 解析 `.vue` 模板 AST。
|
||||||
|
- **安全注入**: 在模板节点中注入源码映射属性,供运行时选择与编辑能力使用。
|
||||||
|
- **安全回写**: 对 `class` 与静态文本进行 AST 级更新,复杂动态表达式场景保护性拒绝。
|
||||||
|
|
||||||
|
### 3.3 客户端逻辑 (Client-Side Logic)
|
||||||
|
- **`SelectionManager`**: 处理点击/悬停事件,并解析 `data-xagi-info` 以提取 `ElementInfo`。
|
||||||
|
- **`EditManager`**: 管理 `contentEditable` 状态,处理保存,并执行列表项同步。
|
||||||
|
- **`DesignModeContext`**: 管理全局状态以及与父窗口(如果在 iframe 中)的通信。
|
||||||
|
- **`DesignModeBridge`**: 处理 iframe 与父级编辑器/IDE 之间的消息传递。
|
||||||
|
- **运行时入口解析**: 插件优先从已安装包解析 React/Vue 客户端入口,确保发布后环境可用。
|
||||||
|
|
||||||
|
## 4. 通信协议 (Communication Protocol)
|
||||||
|
|
||||||
|
### 4.1 Iframe <-> Parent 消息
|
||||||
|
- **`ELEMENT_SELECTED`**: 选中元素时发送。Payload 包含 `ElementInfo`。
|
||||||
|
- **`UPDATE_STYLE`**: 从父级发送以更新 Class。
|
||||||
|
- **`UPDATE_CONTENT`**: 从父级发送以更新文本内容。
|
||||||
|
- **`CONTENT_UPDATED`**: 内容在本地编辑后,从 iframe 发送到父级。
|
||||||
|
- **`STYLE_UPDATED`**: 样式更新后,从 iframe 发送到父级。
|
||||||
|
|
||||||
|
## 5. 服务器中间件 (Server Middleware)
|
||||||
|
- **`/__appdev_design_mode/update`**: 处理源代码更新的 API 端点。
|
||||||
|
- **`codeUpdater.ts`**: 基于行/列信息和规则校验进行源码更新(含文件类型/大小与路径安全校验)。
|
||||||
|
- **路径安全防护**: `serverMiddleware` 与 `codeUpdater` 均校验目标路径必须位于项目根目录内,防止路径穿越。
|
||||||
627
qiming-vite-plugin-design-mode/README.md
Normal file
627
qiming-vite-plugin-design-mode/README.md
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
# Vite Plugin Design Mode
|
||||||
|
|
||||||
|
一个为 AppDev 启用设计模式功能的 Vite 插件,为 React 和 Vue 3 项目提供源码映射与可视化编辑能力(不支持 Vue 2)。
|
||||||
|
|
||||||
|
## 特性
|
||||||
|
|
||||||
|
- **源码映射**: 在编译时自动向DOM元素注入源码位置信息(使用紧凑的 `data-xagi-info` JSON 属性)
|
||||||
|
- **可视化编辑**: 提供直观的设计模式UI,支持实时样式编辑
|
||||||
|
- **双击编辑**: 双击静态文本元素即可进入编辑模式,实时预览并自动保存到源码
|
||||||
|
- **静态内容检测**: 智能识别纯静态文本(无变量、表达式或子元素),只有静态内容才能直接编辑
|
||||||
|
- **列表项同步**: 编辑列表项时自动同步所有相关实例(内容或样式)
|
||||||
|
- **组件识别**: 识别组件名称、导入路径,区分组件使用位置和定义位置
|
||||||
|
- **Vue SFC 支持**: 支持 `.vue` 文件模板注入和 AST 安全回写(class/text)
|
||||||
|
- **实时修改**: 支持实时编辑样式和内容,并自动持久化到源码文件
|
||||||
|
- **Tailwind CSS集成**: 内置Tailwind CSS预设,提供快速样式编辑
|
||||||
|
- **桥接通信**: 提供消息桥接机制,支持与外部工具和设计系统集成(支持 iframe 模式)
|
||||||
|
- **开发服务器集成**: 无缝集成Vite开发服务器,支持热重载
|
||||||
|
- **零配置**: 开箱即用,无需复杂配置即可开始使用
|
||||||
|
|
||||||
|
## 框架支持矩阵
|
||||||
|
|
||||||
|
- React + Vite:支持(自动识别或 `framework: 'react'`)
|
||||||
|
- Vue 3 + Vite:支持(自动识别或 `framework: 'vue'`)
|
||||||
|
- Vue 2 + Vite:不支持(启动时会直接报错,避免出现部分可用的隐性问题)
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
### 一键安装(推荐)
|
||||||
|
|
||||||
|
使用 CLI 工具一键配置插件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @xagi/vite-plugin-design-mode install
|
||||||
|
# 或
|
||||||
|
pnpm dlx @xagi/vite-plugin-design-mode install
|
||||||
|
```
|
||||||
|
|
||||||
|
这个命令会:
|
||||||
|
- 在 `package.json` 的 `devDependencies` 中添加插件依赖
|
||||||
|
- 自动在 `vite.config.ts/js/mjs` 中添加插件配置
|
||||||
|
- 使用默认配置,无需手动传参
|
||||||
|
|
||||||
|
CLI 适用前提:
|
||||||
|
- 项目需为 Vite 项目(`package.json` 中存在 `vite`)
|
||||||
|
- 项目需使用 React 或 Vue 3(`package.json` 中存在 `react`,或 `vue@3.x`)
|
||||||
|
|
||||||
|
**注意:** CLI 只会在配置文件中添加配置,不会执行包管理器安装命令。配置完成后,请手动运行包管理器安装命令来安装依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm install
|
||||||
|
# 或
|
||||||
|
npm install
|
||||||
|
# 或
|
||||||
|
yarn install
|
||||||
|
```
|
||||||
|
|
||||||
|
### 一键卸载
|
||||||
|
|
||||||
|
使用 CLI 工具一键卸载插件并清理配置:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npx @xagi/vite-plugin-design-mode uninstall
|
||||||
|
# 或
|
||||||
|
pnpm dlx @xagi/vite-plugin-design-mode uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
这个命令会:
|
||||||
|
- 从 `package.json` 中移除插件依赖
|
||||||
|
- 从 `vite.config.ts/js/mjs` 中移除 import 和插件配置
|
||||||
|
- 自动清理所有相关配置
|
||||||
|
|
||||||
|
**注意:** CLI 只会在配置文件中移除配置,不会执行包管理器卸载命令。配置清理完成后,请手动运行包管理器卸载命令来移除依赖:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm remove @xagi/vite-plugin-design-mode
|
||||||
|
# 或
|
||||||
|
npm uninstall @xagi/vite-plugin-design-mode
|
||||||
|
# 或
|
||||||
|
yarn remove @xagi/vite-plugin-design-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
### 手动安装
|
||||||
|
|
||||||
|
如果需要手动安装:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install @xagi/vite-plugin-design-mode --save-dev
|
||||||
|
# or
|
||||||
|
yarn add @xagi/vite-plugin-design-mode --dev
|
||||||
|
# or
|
||||||
|
pnpm add @xagi/vite-plugin-design-mode -D
|
||||||
|
```
|
||||||
|
|
||||||
|
### 预发布 / XAGI 集成(推荐)
|
||||||
|
|
||||||
|
`1.1.x` 预发布会频繁发 **`1.1.0-beta.N`**,registry 上历史 beta 会自然变多。**模板、宿主应用、内部文档请不要写死某个 `beta.N`**,否则每发一版就要改依赖。
|
||||||
|
|
||||||
|
集成时请**统一跟 `next` dist-tag**(始终对应当前灰度线):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pnpm add @xagi/vite-plugin-design-mode@next -D
|
||||||
|
# 若直接依赖 client 包(少见),同样使用 @next:
|
||||||
|
# pnpm add @xagi/design-mode-client-vue@next -D
|
||||||
|
```
|
||||||
|
|
||||||
|
`package.json` 里可写 **`"^1.1.0-0"`**(接受 `1.1.0` 线下任意预发版本)并定期 `pnpm update`;或在文档与 CI 中约定:**安装/升级一律执行 `pnpm add @xagi/vite-plugin-design-mode@next -D`**,由 lockfile 固定实际解析版本。
|
||||||
|
|
||||||
|
稳定正式版发布后仍从 **`latest`** 安装即可(与上面预发布通道互不干扰)。
|
||||||
|
|
||||||
|
## 版本发布与 Tag 策略
|
||||||
|
|
||||||
|
为避免预发布版本影响生产用户,仓库采用以下 npm dist-tag 规则:
|
||||||
|
|
||||||
|
- `latest`:仅用于稳定正式版(如 `1.0.37`、`1.1.0`)
|
||||||
|
- `next`:**集成方应使用的预发布通道**;标签指向当前推荐的 `1.1.0-beta.*`(或其它预发 semver),版本号会递增,**请勿在对外文档中要求用户锁定某一枚 `beta.N`**
|
||||||
|
- `beta`:可选的额外 dist-tag(历史或兼容);新集成优先跟 `next`
|
||||||
|
|
||||||
|
### 多包发布 SOP(推荐)
|
||||||
|
|
||||||
|
发布采用统一版本策略,以下 4 个包必须同版本:
|
||||||
|
|
||||||
|
- `@xagi/design-mode-shared`
|
||||||
|
- `@xagi/design-mode-client-react`
|
||||||
|
- `@xagi/design-mode-client-vue`
|
||||||
|
- `@xagi/vite-plugin-design-mode`
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1) 切换 npm 官方源(强烈建议每次发布前显式执行)
|
||||||
|
nrm use npm
|
||||||
|
|
||||||
|
# 2) 如需升级版本,一次性同步全部包版本和内部依赖(版本号按需替换)
|
||||||
|
pnpm run release:version:sync -- 1.1.0-beta.12
|
||||||
|
|
||||||
|
# 3) 一键发布 next(预检 -> 构建 -> 按依赖顺序发布 -> 发布后校验)
|
||||||
|
pnpm run release:next
|
||||||
|
|
||||||
|
# 4) 一键发布 beta(与 next 同流程,但写入 beta tag)
|
||||||
|
pnpm run release:beta
|
||||||
|
|
||||||
|
# 5) 发布前演练(不真正 publish)
|
||||||
|
pnpm run release:next:dry-run
|
||||||
|
pnpm run release:beta:dry-run
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `release:next` / `release:beta` 都会自动执行以下步骤:
|
||||||
|
|
||||||
|
1. `release:preflight`:校验 registry、版本一致性、禁止 `workspace:*` 泄露到发布包
|
||||||
|
2. `release:build`:构建所有包
|
||||||
|
3. `release:publish:*`:按依赖顺序发布(`shared -> react/vue并发 -> plugin`)
|
||||||
|
4. `release:verify:*`:校验对应 dist-tag(`next` 或 `beta`)及目标版本可见性;`verify:next` 对 registry 传播做了**有限次重试**(可用环境变量 `VERIFY_NEXT_MAX_TRIES`、`VERIFY_NEXT_SLEEP_SECS` 调整)
|
||||||
|
|
||||||
|
### 常见问题与修复
|
||||||
|
|
||||||
|
- 报错 `ERR_PNPM_WORKSPACE_PKG_NOT_FOUND`
|
||||||
|
原因:发布包中包含 `workspace:*` 依赖。
|
||||||
|
处理:运行 `release:preflight` 找出问题并改成明确版本号。
|
||||||
|
|
||||||
|
- `next` tag 未指向本次版本
|
||||||
|
处理:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm dist-tag add @xagi/vite-plugin-design-mode@1.1.0-beta.5 next
|
||||||
|
```
|
||||||
|
|
||||||
|
## Basic Usage
|
||||||
|
|
||||||
|
使用一键安装后,插件已自动配置,`vite.config.ts` 中会包含:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import appdevDesignMode from '@xagi/vite-plugin-design-mode';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
appdevDesignMode() // 使用默认配置,无需传参
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
**默认配置说明:**
|
||||||
|
- `enabled: true` - 启用插件
|
||||||
|
- `enableInProduction: false` - 仅在开发环境生效,生产构建时自动禁用
|
||||||
|
- `verbose: true` - 启用详细日志
|
||||||
|
- `attributePrefix: 'data-xagi'` - 源码映射属性的前缀
|
||||||
|
- `include: ['src/**/*.{ts,js,tsx,jsx,vue}']` - 处理 src 目录下的 TypeScript/JavaScript/TSX/JSX/Vue 文件
|
||||||
|
- `exclude: ['node_modules', 'dist']` - 排除指定目录
|
||||||
|
- `enableBackup: false` - 是否启用备份功能
|
||||||
|
- `enableHistory: false` - 是否启用历史记录功能
|
||||||
|
- `framework: 'auto'` - 自动识别 React / Vue 3(可显式设置 `react` 或 `vue`)
|
||||||
|
|
||||||
|
## Advanced Usage
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import appdevDesignMode from '@xagi/vite-plugin-design-mode';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
appdevDesignMode({
|
||||||
|
enabled: true,
|
||||||
|
enableInProduction: false,
|
||||||
|
attributePrefix: 'data-appdev',
|
||||||
|
verbose: true,
|
||||||
|
exclude: ['node_modules', '.git'],
|
||||||
|
include: ['**/*.{js,jsx,ts,tsx}']
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## Vue 3 使用说明
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import vue from '@vitejs/plugin-vue';
|
||||||
|
import appdevDesignMode from '@xagi/vite-plugin-design-mode';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
vue(),
|
||||||
|
appdevDesignMode({
|
||||||
|
framework: 'vue',
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
- `framework: 'vue'` 模式下不会注入 React 运行时 UI 脚本,这是预期行为。
|
||||||
|
- Vue 当前支持模板元素的 `class` 与静态文本回写,复杂动态表达式会被保护性拒绝。
|
||||||
|
- 如检测到 Vue 2(或 `@vitejs/plugin-vue2`),插件会在启动时抛错并提示升级到 Vue 3。
|
||||||
|
|
||||||
|
## 工作原理
|
||||||
|
|
||||||
|
1. **编译时转换**: 插件在编译时按框架选择转换链路
|
||||||
|
- React: 使用 Babel AST 转换 JSX/TSX/JS 文件
|
||||||
|
- Vue: 使用 `@vue/compiler-sfc` + `@vue/compiler-dom` 解析并转换 `.vue` 模板
|
||||||
|
2. **抽象语法树分析**: 基于 AST 识别可编辑元素与源码位置信息
|
||||||
|
- React: 追踪 ImportDeclaration 以解析组件定义和导入路径,并识别 UI 组件(components/ui)
|
||||||
|
- React: 静态分析确定内容是否为纯静态文本(`isStaticContent`)
|
||||||
|
- Vue: 在模板 AST 中定位元素节点,进行注入并用于后续 AST 安全回写
|
||||||
|
3. **源码映射数据注入**: 将源码位置信息作为紧凑的`data-*`属性(默认前缀 `data-xagi`)注入到DOM元素
|
||||||
|
- 主要信息存储在 `data-xagi-info` JSON 属性中(减少 DOM 体积)
|
||||||
|
- 添加 `data-xagi-element-id` 用于唯一标识和列表项同步
|
||||||
|
- 添加 `data-xagi-static-content` 标记可编辑的静态内容
|
||||||
|
4. **虚拟模块加载**: 通过Vite虚拟模块机制动态加载客户端代码
|
||||||
|
5. **运行时通信**: 设计模式UI通过桥接系统与外部工具通信,支持实时编辑
|
||||||
|
- 支持 iframe 模式,通过 `postMessage` 与父窗口通信
|
||||||
|
- 支持元素选择、样式更新、内容更新等消息类型
|
||||||
|
6. **源码持久化**: 修改的样式和内容通过API端点实时写入源码文件
|
||||||
|
- 使用智能文本替换算法,基于行/列信息安全修改源码
|
||||||
|
- 支持样式(className)、内容(innerText)、属性更新
|
||||||
|
7. **列表项同步**: 使用 `element-id` 识别相关列表项,编辑一个实例自动同步到所有实例
|
||||||
|
8. **批量更新**: 支持批量更新多个元素,通过防抖机制优化性能
|
||||||
|
9. **备份与历史**: 可选启用备份和历史记录功能,支持撤销操作
|
||||||
|
|
||||||
|
## API 端点
|
||||||
|
|
||||||
|
插件提供以下API端点:
|
||||||
|
|
||||||
|
### 基础端点
|
||||||
|
- `GET /__appdev_design_mode/get-source?elementId=xxx` - 获取指定元素的源码信息和文件内容(增强版,包含更多元数据)
|
||||||
|
- `POST /__appdev_design_mode/modify-source` - 修改源码文件中的样式或内容(兼容旧版本)
|
||||||
|
- `POST /__appdev_design_mode/update` - 统一更新接口,支持样式、内容、属性更新
|
||||||
|
- `GET /__appdev_design_mode/health` - 健康检查,返回插件运行状态
|
||||||
|
|
||||||
|
### 批量操作端点
|
||||||
|
- `POST /__appdev_design_mode/batch-update` - 批量更新多个元素的源码(需要 `enableHistory: true` 以保存会话)
|
||||||
|
- `GET /__appdev_design_mode/batch-update-status?batchId=xxx` - 查询批量更新会话状态(需要 `enableHistory: true`)
|
||||||
|
|
||||||
|
### 历史记录端点(需要 `enableHistory: true`)
|
||||||
|
- `GET /__appdev_design_mode/get-history` - 获取更新历史记录
|
||||||
|
- `POST /__appdev_design_mode/undo` - 撤销操作(需要 `enableBackup: true`)
|
||||||
|
- `POST /__appdev_design_mode/redo` - 重做操作(暂未实现)
|
||||||
|
|
||||||
|
### 验证端点
|
||||||
|
- `POST /__appdev_design_mode/validate-update` - 验证更新请求的有效性
|
||||||
|
|
||||||
|
## 生成属性
|
||||||
|
|
||||||
|
插件处理的元素将具有以下属性(默认前缀为 `data-xagi`,可通过 `attributePrefix` 配置项自定义):
|
||||||
|
|
||||||
|
### 核心属性(实际注入)
|
||||||
|
|
||||||
|
- **`{prefix}-info`**: 包含完整源码映射信息的 JSON 字符串,包含以下字段:
|
||||||
|
- `fileName`: 源码文件路径
|
||||||
|
- `lineNumber`: 源码行号
|
||||||
|
- `columnNumber`: 源码列号
|
||||||
|
- `elementType`: 元素类型(标签名)
|
||||||
|
- `componentName`: 组件名称(如果适用)
|
||||||
|
- `functionName`: 函数名称(如果适用)
|
||||||
|
- `elementId`: 唯一元素标识符
|
||||||
|
- `importPath`: 组件导入路径(用于区分组件使用位置和定义位置)
|
||||||
|
- `isUIComponent`: 是否是 UI 组件(components/ui 目录下的组件)
|
||||||
|
|
||||||
|
- **`{prefix}-element-id`**: 唯一元素标识符,格式为 `文件路径:行号:列号_标签名#ID`(如果有 id 属性)或 `文件路径:行号:列号_标签名`(如果没有 id 属性)。用于列表项同步和元素识别。
|
||||||
|
|
||||||
|
- **`{prefix}-position`**: 位置信息,格式为 `行号:列号`(简化版位置信息)
|
||||||
|
|
||||||
|
- **`{prefix}-static-content`**: 标记元素是否包含纯静态内容(值为 `'true'`),用于判断元素是否可以直接编辑。只有包含纯静态文本(无变量、表达式或子元素)的元素才会被标记。
|
||||||
|
|
||||||
|
- **`{prefix}-static-class`**: 标记 className 是否为纯静态字符串(值为 `'true'`),用于判断样式是否可以直接编辑。
|
||||||
|
|
||||||
|
- **`{prefix}-children-source`**: 子元素的源码位置信息,格式为 `文件路径:行号:列号`(用于透传组件追踪静态文本子元素的来源位置)
|
||||||
|
|
||||||
|
### 优化说明
|
||||||
|
|
||||||
|
为了减少 DOM 体积,插件采用了紧凑的属性策略:
|
||||||
|
- **所有主要信息都包含在 `{prefix}-info` 中**,不再单独注入 `{prefix}-file`、`{prefix}-line`、`{prefix}-column`、`{prefix}-component`、`{prefix}-function`、`{prefix}-import` 等属性
|
||||||
|
- 这些信息可以通过解析 `{prefix}-info` JSON 字符串获取
|
||||||
|
|
||||||
|
**注意**:这些属性使用可配置的前缀,默认值为 `data-xagi`。通过设置 `attributePrefix` 选项,可以避免与用户自定义的 `data-*` 属性冲突。例如,设置 `attributePrefix: 'data-appdev'` 后,属性名将变为 `data-appdev-info`、`data-appdev-element-id`、`data-appdev-static-content` 等。
|
||||||
|
|
||||||
|
## 浏览器集成
|
||||||
|
|
||||||
|
### 直接DOM访问
|
||||||
|
|
||||||
|
在浏览器开发者工具或外部应用中:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 获取元素源码信息(注意:默认前缀已改为 data-xagi)
|
||||||
|
const element = document.querySelector('.my-element');
|
||||||
|
// 从全局配置获取前缀,或使用默认值 'data-xagi'
|
||||||
|
const prefix = window.__APPDEV_DESIGN_MODE_CONFIG__?.attributePrefix || 'data-xagi';
|
||||||
|
const sourceInfo = element.getAttribute(`${prefix}-info`);
|
||||||
|
const sourceData = JSON.parse(sourceInfo);
|
||||||
|
|
||||||
|
console.log(sourceData);
|
||||||
|
// {
|
||||||
|
// fileName: 'src/App.tsx',
|
||||||
|
// lineNumber: 10,
|
||||||
|
// columnNumber: 5,
|
||||||
|
// elementType: 'div',
|
||||||
|
// componentName: 'App',
|
||||||
|
// functionName: 'App',
|
||||||
|
// elementId: 'src/App.tsx:10:5_div',
|
||||||
|
// importPath: undefined, // 如果是组件,会显示导入路径
|
||||||
|
// isUIComponent: false // 是否是 UI 组件
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 检查元素是否可以直接编辑(静态内容)
|
||||||
|
const isEditable = element.getAttribute(`${prefix}-static-content`) === 'true';
|
||||||
|
if (isEditable) {
|
||||||
|
// 双击元素即可编辑
|
||||||
|
element.addEventListener('dblclick', () => {
|
||||||
|
element.contentEditable = 'true';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 桥接通信
|
||||||
|
|
||||||
|
使用内置桥接系统与设计模式UI交互:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 监听元素选择变化
|
||||||
|
window.addEventListener('message', (event) => {
|
||||||
|
const { type, payload } = event.data;
|
||||||
|
|
||||||
|
if (type === 'SELECTION_CHANGED') {
|
||||||
|
console.log('选中的元素:', payload);
|
||||||
|
// payload 包含: tagName, id, className, innerText, source
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送样式更新命令
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'UPDATE_STYLE',
|
||||||
|
payload: {
|
||||||
|
newClass: 'bg-blue-500 text-white p-4 rounded-lg'
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
|
||||||
|
// 发送内容更新命令
|
||||||
|
window.parent.postMessage({
|
||||||
|
type: 'UPDATE_CONTENT',
|
||||||
|
payload: {
|
||||||
|
newContent: '更新后的文本内容'
|
||||||
|
}
|
||||||
|
}, '*');
|
||||||
|
```
|
||||||
|
|
||||||
|
## 配置选项
|
||||||
|
|
||||||
|
| 选项 | 类型 | 默认值 | 描述 |
|
||||||
|
|------|------|--------|------|
|
||||||
|
| `enabled` | boolean | `true` | 是否启用设计模式插件 |
|
||||||
|
| `enableInProduction` | boolean | `false` | 是否在生产环境启用,通常保持false |
|
||||||
|
| `attributePrefix` | string | `'data-xagi'` | 自定义源码映射属性的前缀 |
|
||||||
|
| `verbose` | boolean | `true` | 是否启用详细日志输出,便于调试 |
|
||||||
|
| `exclude` | string[] | `['node_modules', 'dist']` | 排除处理的文件模式和目录 |
|
||||||
|
| `include` | string[] | `['src/**/*.{ts,js,tsx,jsx}']` | 包含处理的文件模式,支持glob语法 |
|
||||||
|
| `enableBackup` | boolean | `false` | 是否启用备份功能,启用后会在修改源码前创建备份文件 |
|
||||||
|
| `enableHistory` | boolean | `false` | 是否启用历史记录功能,启用后会保存批量更新会话和历史记录 |
|
||||||
|
|
||||||
|
### 配置示例
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// vite.config.ts
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
appdevDesignMode({
|
||||||
|
enabled: true,
|
||||||
|
enableInProduction: false,
|
||||||
|
attributePrefix: 'data-appdev',
|
||||||
|
verbose: true,
|
||||||
|
exclude: ['node_modules', '.git', 'dist'],
|
||||||
|
include: ['src/**/*.{ts,js,tsx,jsx}', 'pages/**/*.{ts,js,tsx,jsx}'],
|
||||||
|
enableBackup: true, // 启用备份功能
|
||||||
|
enableHistory: true // 启用历史记录功能
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用场景
|
||||||
|
|
||||||
|
### 1. 设计与开发协作
|
||||||
|
- 设计师可以直接在浏览器中调整组件样式
|
||||||
|
- 实时预览设计效果,所见即所得
|
||||||
|
- 自动生成样式代码,无需手动编写
|
||||||
|
|
||||||
|
### 2. 组件库开发
|
||||||
|
- 快速迭代组件样式和变体
|
||||||
|
- 可视化调整组件参数和样式
|
||||||
|
- 生成一致的设计语言
|
||||||
|
|
||||||
|
### 3. 原型开发
|
||||||
|
- 快速构建和调整页面布局
|
||||||
|
- 实时验证设计想法
|
||||||
|
- 加速从原型到代码的转换
|
||||||
|
|
||||||
|
### 4. 团队协作
|
||||||
|
- 统一设计系统和代码规范
|
||||||
|
- 减少设计师和开发者的沟通成本
|
||||||
|
- 提高开发效率和代码质量
|
||||||
|
|
||||||
|
## Tailwind CSS 预设
|
||||||
|
|
||||||
|
插件内置了常用的Tailwind CSS类预设:
|
||||||
|
|
||||||
|
### 背景颜色预设
|
||||||
|
- `bg-white` - 白色背景
|
||||||
|
- `bg-slate-50` - 浅灰色背景
|
||||||
|
- `bg-blue-50` - 浅蓝色背景
|
||||||
|
- `bg-blue-100` - 中浅蓝色背景
|
||||||
|
- `bg-blue-600` - 深蓝色背景
|
||||||
|
- `bg-red-50` - 浅红色背景
|
||||||
|
- `bg-green-50` - 浅绿色背景
|
||||||
|
|
||||||
|
### 文字颜色预设
|
||||||
|
- `text-slate-900` - 深灰色文字
|
||||||
|
- `text-slate-600` - 中灰色文字
|
||||||
|
- `text-blue-600` - 蓝色文字
|
||||||
|
- `text-white` - 白色文字
|
||||||
|
- `text-red-600` - 红色文字
|
||||||
|
|
||||||
|
### 间距预设
|
||||||
|
- `p-0` - 无内边距
|
||||||
|
- `p-2` - 小内边距
|
||||||
|
- `p-4` - 中等内边距
|
||||||
|
- `p-6` - 大内边距
|
||||||
|
- `p-8` - 特大内边距
|
||||||
|
- `p-12` - 超大内边距
|
||||||
|
|
||||||
|
### 圆角预设
|
||||||
|
- `rounded-none` - 无圆角
|
||||||
|
- `rounded-sm` - 小圆角
|
||||||
|
- `rounded-md` - 中等圆角
|
||||||
|
- `rounded-lg` - 大圆角
|
||||||
|
- `rounded-full` - 完全圆角
|
||||||
|
|
||||||
|
## 设计模式UI使用指南
|
||||||
|
|
||||||
|
### 启动设计模式
|
||||||
|
1. 在页面右下角找到设计模式开关
|
||||||
|
2. 点击开关启用设计模式
|
||||||
|
3. 开始选择和编辑页面元素
|
||||||
|
|
||||||
|
### 编辑元素样式
|
||||||
|
1. 点击页面中的任意元素
|
||||||
|
2. 在右侧编辑面板中选择预设样式
|
||||||
|
3. 实时预览样式效果
|
||||||
|
4. 修改会自动保存到源码文件
|
||||||
|
5. 对于列表项,修改一个实例会自动同步到所有相关实例
|
||||||
|
|
||||||
|
### 编辑元素内容
|
||||||
|
1. **双击**包含静态文本的元素(只有标记了 `data-xagi-static-content="true"` 的元素才能编辑)
|
||||||
|
2. 进入 `contentEditable` 模式,直接编辑文本
|
||||||
|
3. 失去焦点时自动保存到源码文件
|
||||||
|
4. 对于列表项,编辑一个实例会自动同步到所有相关实例
|
||||||
|
|
||||||
|
### 重置修改
|
||||||
|
- 点击“重置所有修改”按钮撤销所有更改
|
||||||
|
- 页面将重新加载并恢复原始状态
|
||||||
|
- 如果启用了备份功能(`enableBackup: true`),可以使用撤销功能恢复之前的版本
|
||||||
|
- 如果启用了历史记录功能(`enableHistory: true`),可以查看和恢复历史更新会话
|
||||||
|
|
||||||
|
## 故障排除
|
||||||
|
|
||||||
|
### 常见问题
|
||||||
|
|
||||||
|
**Q: 设计模式UI没有显示**
|
||||||
|
A: 检查插件是否正确配置,确保在开发模式下运行
|
||||||
|
|
||||||
|
**Q: 点击元素没有反应**
|
||||||
|
A: 确认已经启用设计模式,检查浏览器控制台是否有错误信息
|
||||||
|
|
||||||
|
**Q: 样式修改没有保存**
|
||||||
|
A: 检查文件权限,确保有写入权限,或者检查开发服务器状态
|
||||||
|
|
||||||
|
**Q: 源码映射不准确**
|
||||||
|
A: 检查是否正确配置了include模式,确保源文件被正确处理
|
||||||
|
|
||||||
|
### 调试技巧
|
||||||
|
|
||||||
|
1. 启用verbose模式查看详细日志
|
||||||
|
2. 检查浏览器开发者工具的网络面板,确认API请求状态
|
||||||
|
3. 查看元素是否正确生成了源码映射属性
|
||||||
|
4. 使用健康检查端点验证插件运行状态
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查插件状态
|
||||||
|
curl http://localhost:3000/__appdev_design_mode/health
|
||||||
|
```
|
||||||
|
|
||||||
|
## 开发指南
|
||||||
|
|
||||||
|
### 本地开发
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 克隆项目
|
||||||
|
git clone <repository-url>
|
||||||
|
cd vite-plugin-design-mode
|
||||||
|
|
||||||
|
# 安装依赖
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# 运行示例
|
||||||
|
cd examples/advanced
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### 自定义预设
|
||||||
|
|
||||||
|
如需自定义样式预设,可以修改客户端配置:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
// 在 DesignModeUI.tsx 中自定义预设
|
||||||
|
const CUSTOM_PRESETS = {
|
||||||
|
bgColors: [
|
||||||
|
{ label: 'Primary', value: 'bg-blue-600' },
|
||||||
|
{ label: 'Secondary', value: 'bg-gray-600' },
|
||||||
|
// 添加更多自定义颜色
|
||||||
|
]
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
## 贡献指南
|
||||||
|
|
||||||
|
欢迎提交Issue和Pull Request来改进这个插件!
|
||||||
|
|
||||||
|
### 开发流程
|
||||||
|
1. Fork项目
|
||||||
|
2. 创建功能分支
|
||||||
|
3. 提交更改
|
||||||
|
4. 创建Pull Request
|
||||||
|
|
||||||
|
### 代码规范
|
||||||
|
- 使用TypeScript
|
||||||
|
- 遵循ESLint规则
|
||||||
|
- 添加适当的测试
|
||||||
|
- 更新相关文档
|
||||||
|
|
||||||
|
## 更新日志
|
||||||
|
|
||||||
|
完整变更记录见 [`CHANGELOG.md`](./CHANGELOG.md)。
|
||||||
|
|
||||||
|
### v1.1.0-beta.5
|
||||||
|
- 修复 npm 发布产物中的 workspace 依赖协议问题,确保外部使用 pnpm/npm 安装可正常解析依赖
|
||||||
|
|
||||||
|
### v1.1.0-beta.4
|
||||||
|
- 修复插件发布后客户端运行时入口解析,优先从安装包解析 React/Vue runtime
|
||||||
|
- 增加路径安全校验,避免通过路径前缀绕过项目根目录限制
|
||||||
|
- 完成多包迁移后的测试回归修复,测试全量通过
|
||||||
|
|
||||||
|
### v1.1.0-beta.3
|
||||||
|
- 完成项目向 `packages/*` 多包结构迁移
|
||||||
|
- 保留并迁移 CLI 到 plugin 包内,发布产物包含 CLI
|
||||||
|
- 调整 examples/test 引用路径,移除根 `src` 目录实现
|
||||||
|
|
||||||
|
### v1.1.0-beta.2
|
||||||
|
- 新增 React/Vue 3 动态识别支持(`framework: 'auto'`)
|
||||||
|
- CLI 支持在 Vue 3 + Vite 项目安装
|
||||||
|
- 检测到 Vue 2(含 `@vitejs/plugin-vue2`)时快速失败并给出升级提示
|
||||||
|
- 发布策略调整:`latest` 仅稳定版,预发布使用 `beta/next`
|
||||||
|
|
||||||
|
### v1.0.36
|
||||||
|
- 更新默认属性前缀为 `data-xagi`
|
||||||
|
- 新增批量更新功能(`/batch-update` 端点)
|
||||||
|
- 新增历史记录功能(`enableHistory` 配置项)
|
||||||
|
- 新增备份功能(`enableBackup` 配置项)
|
||||||
|
- 新增撤销/重做功能(需要启用备份和历史记录)
|
||||||
|
- 增强 `/get-source` 端点,返回更多元数据
|
||||||
|
- 新增 `/validate-update` 端点用于验证更新请求
|
||||||
|
- 支持 TypeScript/JavaScript/TSX/JSX 文件处理
|
||||||
|
- 优化默认文件包含模式,支持更多文件类型
|
||||||
|
- 改进CLI工具,支持一键安装和卸载
|
||||||
|
|
||||||
|
### v1.0.0
|
||||||
|
- 初始版本发布
|
||||||
|
- 实现基本的源码映射功能
|
||||||
|
- 添加可视化设计模式UI
|
||||||
|
- 支持实时样式编辑和源码修改
|
||||||
|
- 集成Tailwind CSS预设
|
||||||
|
|
||||||
|
## License
|
||||||
|
|
||||||
|
MIT
|
||||||
277
qiming-vite-plugin-design-mode/TESTING_CLI.md
Normal file
277
qiming-vite-plugin-design-mode/TESTING_CLI.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# CLI 功能本地测试指南
|
||||||
|
|
||||||
|
本文档说明如何在本地测试 `@xagi/vite-plugin-design-mode` 的 CLI 安装和卸载功能。
|
||||||
|
|
||||||
|
## 前置准备
|
||||||
|
|
||||||
|
1. 确保项目已安装依赖:
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
2. 构建项目(编译 TypeScript 到 dist 目录):
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
这会编译所有源代码到 `dist/` 目录,包括 CLI 文件。
|
||||||
|
|
||||||
|
## 测试方法
|
||||||
|
|
||||||
|
### 方法 1: 使用 npm link(推荐)
|
||||||
|
|
||||||
|
这是最接近真实使用场景的测试方法。
|
||||||
|
|
||||||
|
#### 步骤 1: 在项目根目录创建链接
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在项目根目录执行
|
||||||
|
npm link
|
||||||
|
```
|
||||||
|
|
||||||
|
这会在全局创建一个符号链接,指向当前项目。
|
||||||
|
|
||||||
|
#### 步骤 2: 在测试项目中链接插件
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入测试项目目录(例如 examples/demo)
|
||||||
|
cd examples/demo
|
||||||
|
|
||||||
|
# 链接插件
|
||||||
|
npm link @xagi/vite-plugin-design-mode
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 3: 测试安装命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在测试项目目录执行
|
||||||
|
npx @xagi/vite-plugin-design-mode install
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 4: 测试卸载命令
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在测试项目目录执行
|
||||||
|
npx @xagi/vite-plugin-design-mode uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 2: 直接使用 node 运行编译后的文件
|
||||||
|
|
||||||
|
如果不想使用 npm link,可以直接运行编译后的 CLI 文件。
|
||||||
|
|
||||||
|
#### 步骤 1: 构建项目
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 步骤 2: 在测试项目中运行 CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 进入测试项目目录
|
||||||
|
cd examples/demo
|
||||||
|
|
||||||
|
# 直接运行编译后的 CLI 文件
|
||||||
|
node ../../packages/plugin/dist/cli/index.js install
|
||||||
|
# 或
|
||||||
|
node ../../packages/plugin/dist/cli/index.js uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
### 方法 3: 使用 tsx 直接运行 TypeScript 文件(开发调试)
|
||||||
|
|
||||||
|
在开发过程中,可以使用 `tsx` 直接运行 TypeScript 文件,无需先编译。
|
||||||
|
|
||||||
|
#### 安装 tsx(如果还没有)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install -g tsx
|
||||||
|
# 或
|
||||||
|
npm install --save-dev tsx
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 运行 CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在项目根目录
|
||||||
|
tsx packages/plugin/src/cli/index.ts install
|
||||||
|
|
||||||
|
# 或指定工作目录为测试项目
|
||||||
|
cd examples/demo
|
||||||
|
tsx ../../packages/plugin/src/cli/index.ts install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 完整测试流程示例
|
||||||
|
|
||||||
|
以下是在 `examples/demo` 项目中完整测试安装和卸载的流程:
|
||||||
|
|
||||||
|
### 1. 准备测试环境
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 在项目根目录构建
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# 进入测试项目
|
||||||
|
cd examples/demo
|
||||||
|
|
||||||
|
# 备份当前的 vite.config.ts(可选)
|
||||||
|
cp vite.config.ts vite.config.ts.backup
|
||||||
|
|
||||||
|
# 备份 package.json(可选)
|
||||||
|
cp package.json package.json.backup
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 测试安装功能
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 方法 A: 使用 npm link
|
||||||
|
npm link @xagi/vite-plugin-design-mode
|
||||||
|
npx @xagi/vite-plugin-design-mode install
|
||||||
|
|
||||||
|
# 方法 B: 直接运行编译后的文件
|
||||||
|
node ../../packages/plugin/dist/cli/index.js install
|
||||||
|
|
||||||
|
# 方法 C: 使用 tsx(开发时)
|
||||||
|
tsx ../../packages/plugin/src/cli/index.ts install
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证安装结果:**
|
||||||
|
|
||||||
|
1. 检查 `package.json` 中是否添加了 `@xagi/vite-plugin-design-mode` 依赖
|
||||||
|
2. 检查 `vite.config.ts` 中是否添加了:
|
||||||
|
- `import appdevDesignMode from '@xagi/vite-plugin-design-mode';`
|
||||||
|
- `appdevDesignMode()` 在 plugins 数组中
|
||||||
|
|
||||||
|
### 3. 测试卸载功能
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用相同的方法运行卸载命令
|
||||||
|
npx @xagi/vite-plugin-design-mode uninstall
|
||||||
|
# 或
|
||||||
|
node ../../packages/plugin/dist/cli/index.js uninstall
|
||||||
|
# 或
|
||||||
|
tsx ../../packages/plugin/src/cli/index.ts uninstall
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证卸载结果:**
|
||||||
|
|
||||||
|
1. 检查 `package.json` 中是否移除了 `@xagi/vite-plugin-design-mode` 依赖
|
||||||
|
2. 检查 `vite.config.ts` 中是否移除了:
|
||||||
|
- import 语句
|
||||||
|
- `appdevDesignMode()` 调用
|
||||||
|
|
||||||
|
### 4. 恢复测试环境(可选)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 如果需要恢复到原始状态
|
||||||
|
cp vite.config.ts.backup vite.config.ts
|
||||||
|
cp package.json.backup package.json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试不同场景
|
||||||
|
|
||||||
|
### 场景 1: 全新安装(插件未安装)
|
||||||
|
|
||||||
|
1. 确保测试项目的 `package.json` 中没有 `@xagi/vite-plugin-design-mode`
|
||||||
|
2. 确保 `vite.config.ts` 中没有相关配置
|
||||||
|
3. 运行 `install` 命令
|
||||||
|
4. 验证插件已安装且配置已添加
|
||||||
|
|
||||||
|
### 场景 2: 升级已安装的插件
|
||||||
|
|
||||||
|
1. 先在测试项目中安装旧版本(如果有)
|
||||||
|
2. 运行 `install` 命令
|
||||||
|
3. 验证插件已升级到最新版本
|
||||||
|
|
||||||
|
### 场景 3: 卸载已安装的插件
|
||||||
|
|
||||||
|
1. 确保插件已安装
|
||||||
|
2. 运行 `uninstall` 命令
|
||||||
|
3. 验证插件和配置都已移除
|
||||||
|
|
||||||
|
### 场景 4: 测试不同的包管理器
|
||||||
|
|
||||||
|
在不同使用不同包管理器的项目中测试:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# npm 项目
|
||||||
|
cd examples/basic
|
||||||
|
node ../../packages/plugin/dist/cli/index.js install
|
||||||
|
|
||||||
|
# pnpm 项目
|
||||||
|
cd examples/demo # 如果有 pnpm-lock.yaml
|
||||||
|
node ../../packages/plugin/dist/cli/index.js install
|
||||||
|
|
||||||
|
# yarn 项目(如果有 yarn.lock)
|
||||||
|
node ../../packages/plugin/dist/cli/index.js install
|
||||||
|
```
|
||||||
|
|
||||||
|
## 调试技巧
|
||||||
|
|
||||||
|
### 查看详细输出
|
||||||
|
|
||||||
|
CLI 工具会输出详细的执行信息,包括:
|
||||||
|
- 检测到的包管理器
|
||||||
|
- 插件安装状态
|
||||||
|
- 执行的命令
|
||||||
|
- 配置文件修改情况
|
||||||
|
|
||||||
|
### 检查编译后的文件
|
||||||
|
|
||||||
|
确保 CLI 文件已正确编译:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查 dist/cli 目录
|
||||||
|
ls -la dist/cli/
|
||||||
|
|
||||||
|
# 应该看到:
|
||||||
|
# - index.js
|
||||||
|
# - install.js
|
||||||
|
# - uninstall.js
|
||||||
|
```
|
||||||
|
|
||||||
|
### 常见问题排查
|
||||||
|
|
||||||
|
1. **命令未找到**
|
||||||
|
- 确保已运行 `npm run build`
|
||||||
|
- 检查 `packages/plugin/dist/cli/index.js` 是否存在
|
||||||
|
- 如果使用 npm link,确保已正确链接
|
||||||
|
|
||||||
|
2. **权限问题**
|
||||||
|
- 确保有写入 `package.json` 和 `vite.config.ts` 的权限
|
||||||
|
|
||||||
|
3. **配置文件格式问题**
|
||||||
|
- CLI 会尝试处理不同的代码风格,但某些复杂格式可能需要手动调整
|
||||||
|
|
||||||
|
## 自动化测试脚本(可选)
|
||||||
|
|
||||||
|
可以创建一个测试脚本来自动化测试流程:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
# test-cli.sh
|
||||||
|
|
||||||
|
echo "构建项目..."
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
echo "进入测试项目..."
|
||||||
|
cd examples/demo
|
||||||
|
|
||||||
|
echo "测试安装..."
|
||||||
|
node ../../packages/plugin/dist/cli/index.js install
|
||||||
|
|
||||||
|
echo "等待 2 秒..."
|
||||||
|
sleep 2
|
||||||
|
|
||||||
|
echo "测试卸载..."
|
||||||
|
node ../../packages/plugin/dist/cli/index.js uninstall
|
||||||
|
|
||||||
|
echo "测试完成!"
|
||||||
|
```
|
||||||
|
|
||||||
|
保存为 `test-cli.sh`,然后运行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
chmod +x test-cli.sh
|
||||||
|
./test-cli.sh
|
||||||
|
```
|
||||||
|
|
||||||
14
qiming-vite-plugin-design-mode/examples/advanced/index.html
Normal file
14
qiming-vite-plugin-design-mode/examples/advanced/index.html
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Advanced AppDev Design Mode Example</title>
|
||||||
|
<meta name="description" content="高级AppDev设计模式插件示例,展示复杂的使用场景" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="root" data-app-root="main"></div>
|
||||||
|
<script type="module" src="/src/main.tsx"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
3148
qiming-vite-plugin-design-mode/examples/advanced/package-lock.json
generated
Normal file
3148
qiming-vite-plugin-design-mode/examples/advanced/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
|||||||
|
{
|
||||||
|
"name": "vite-plugin-appdev-design-mode-advanced-example",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite",
|
||||||
|
"build": "tsc && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"type-check": "tsc --noEmit"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@radix-ui/react-dialog": "^1.1.15",
|
||||||
|
"@radix-ui/react-slider": "^1.3.6",
|
||||||
|
"@radix-ui/react-slot": "^1.2.4",
|
||||||
|
"@radix-ui/react-switch": "^1.2.6",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"react": "^18.2.0",
|
||||||
|
"react-dom": "^18.2.0",
|
||||||
|
"react-router-dom": "^6.8.0",
|
||||||
|
"tailwind-merge": "^3.4.0",
|
||||||
|
"zustand": "^4.3.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4.1.17",
|
||||||
|
"@types/react": "^18.2.0",
|
||||||
|
"@types/react-dom": "^18.2.0",
|
||||||
|
"@vitejs/plugin-react": "^4.0.0",
|
||||||
|
"autoprefixer": "^10.4.22",
|
||||||
|
"postcss": "^8.5.6",
|
||||||
|
"tailwindcss": "^4.1.17",
|
||||||
|
"typescript": "^5.0.0",
|
||||||
|
"vite": "^5.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
export default {
|
||||||
|
plugins: {
|
||||||
|
'@tailwindcss/postcss': {},
|
||||||
|
autoprefixer: {},
|
||||||
|
},
|
||||||
|
}
|
||||||
119
qiming-vite-plugin-design-mode/examples/advanced/src/App.tsx
Normal file
119
qiming-vite-plugin-design-mode/examples/advanced/src/App.tsx
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import React, { Suspense } from 'react';
|
||||||
|
import { Routes, Route, Link, useLocation } from 'react-router-dom';
|
||||||
|
import { useAppStore } from './store/appStore';
|
||||||
|
|
||||||
|
// 页面组件
|
||||||
|
const Dashboard = React.lazy(() => import('./pages/Dashboard'));
|
||||||
|
const ComponentShowcase = React.lazy(() => import('./pages/ComponentShowcase'));
|
||||||
|
const InteractiveDemo = React.lazy(() => import('./pages/InteractiveDemo'));
|
||||||
|
const ConfigurationGuide = React.lazy(() => import('./pages/ConfigurationGuide'));
|
||||||
|
|
||||||
|
// 通用组件
|
||||||
|
const LoadingSpinner: React.FC = () => (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
padding: '2rem'
|
||||||
|
}}>
|
||||||
|
<div style={{
|
||||||
|
width: '40px',
|
||||||
|
height: '40px',
|
||||||
|
border: '3px solid #f3f3f3',
|
||||||
|
borderTop: '3px solid #3498db',
|
||||||
|
borderRadius: '50%',
|
||||||
|
animation: 'spin 1s linear infinite'
|
||||||
|
}} />
|
||||||
|
<style>{`
|
||||||
|
@keyframes spin {
|
||||||
|
0% { transform: rotate(0deg); }
|
||||||
|
100% { transform: rotate(360deg); }
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
const Navigation: React.FC = () => {
|
||||||
|
const location = useLocation();
|
||||||
|
const { theme, toggleTheme } = useAppStore();
|
||||||
|
|
||||||
|
const navItems = [
|
||||||
|
{ path: '/', label: 'Dashboard', icon: '🏠' },
|
||||||
|
{ path: '/showcase', label: 'Component Showcase', icon: '🎨' },
|
||||||
|
{ path: '/interactive', label: 'Interactive Demo', icon: '🎯' },
|
||||||
|
{ path: '/config', label: 'Configuration', icon: '⚙️' }
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<nav className={`nav ${theme}`} data-component="navigation">
|
||||||
|
<div className="nav-brand" data-element="brand">
|
||||||
|
<h1>Advanced Design Mode</h1>
|
||||||
|
<span className="version">v1.0.0</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nav-links" data-element="nav-links">
|
||||||
|
{navItems.map(item => (
|
||||||
|
<Link
|
||||||
|
key={item.path}
|
||||||
|
to={item.path}
|
||||||
|
className={`nav-link ${location.pathname === item.path ? 'active' : ''}`}
|
||||||
|
data-nav-item={item.path.replace('/', '') || 'dashboard'}
|
||||||
|
>
|
||||||
|
<span className="icon">{item.icon}</span>
|
||||||
|
<span className="label">{item.label}</span>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="nav-actions" data-element="actions">
|
||||||
|
<button
|
||||||
|
className="theme-toggle"
|
||||||
|
onClick={toggleTheme}
|
||||||
|
data-action="toggle-theme"
|
||||||
|
title="Toggle theme"
|
||||||
|
>
|
||||||
|
{theme === 'light' ? '🌙' : '☀️'}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const App: React.FC = () => {
|
||||||
|
const { isLoading, error } = useAppStore();
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div className="app-error">
|
||||||
|
<h1>Application Error</h1>
|
||||||
|
<p>{error}</p>
|
||||||
|
<button onClick={() => window.location.reload()}>
|
||||||
|
Reload Application
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={`app ${useAppStore.getState().theme}`} data-app="advanced-demo">
|
||||||
|
<Navigation />
|
||||||
|
|
||||||
|
<main className="main-content" data-component="main">
|
||||||
|
<Suspense fallback={<LoadingSpinner />}>
|
||||||
|
<Routes>
|
||||||
|
<Route path="/" element={<Dashboard />} />
|
||||||
|
<Route path="/showcase" element={<ComponentShowcase />} />
|
||||||
|
<Route path="/interactive" element={<InteractiveDemo />} />
|
||||||
|
<Route path="/config" element={<ConfigurationGuide />} />
|
||||||
|
</Routes>
|
||||||
|
</Suspense>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className="app-footer" data-component="footer">
|
||||||
|
<p>Built with Vite + React + TypeScript + AppDev Design Mode Plugin</p>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default App;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { cn } from '../utils/cn';
|
||||||
|
|
||||||
|
export interface DemoElementProps {
|
||||||
|
element: {
|
||||||
|
id: string;
|
||||||
|
tag: string;
|
||||||
|
content: string;
|
||||||
|
className: string;
|
||||||
|
};
|
||||||
|
isDesignMode: boolean;
|
||||||
|
onSelect: (element: HTMLElement) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DemoElement: React.FC<DemoElementProps> = ({ element, isDesignMode, onSelect }) => {
|
||||||
|
const Tag = element.tag as any;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tag
|
||||||
|
id={element.id}
|
||||||
|
className={cn(
|
||||||
|
element.className,
|
||||||
|
"transition-all duration-200 cursor-default",
|
||||||
|
isDesignMode && "hover:outline hover:outline-2 hover:outline-blue-400 hover:outline-offset-2 cursor-pointer",
|
||||||
|
"data-[selected=true]:outline data-[selected=true]:outline-2 data-[selected=true]:outline-blue-600 data-[selected=true]:outline-offset-2"
|
||||||
|
)}
|
||||||
|
onClick={(e: React.MouseEvent) => {
|
||||||
|
if (isDesignMode) {
|
||||||
|
e.preventDefault();
|
||||||
|
onSelect(e.currentTarget as HTMLElement);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
data-element={element.id}
|
||||||
|
>
|
||||||
|
{element.content}
|
||||||
|
</Tag>
|
||||||
|
);
|
||||||
|
};
|
||||||
957
qiming-vite-plugin-design-mode/examples/advanced/src/index.css
Normal file
957
qiming-vite-plugin-design-mode/examples/advanced/src/index.css
Normal file
@@ -0,0 +1,957 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
/* Advanced AppDev Design Mode Example Styles */
|
||||||
|
|
||||||
|
/* CSS Variables for theming */
|
||||||
|
:root {
|
||||||
|
/* Light theme colors */
|
||||||
|
--bg-primary: #ffffff;
|
||||||
|
--bg-secondary: #f8fafc;
|
||||||
|
--bg-tertiary: #f1f5f9;
|
||||||
|
--text-primary: #1e293b;
|
||||||
|
--text-secondary: #64748b;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
--border-color: #e2e8f0;
|
||||||
|
--border-hover: #cbd5e1;
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||||||
|
--accent-primary: #3b82f6;
|
||||||
|
--accent-secondary: #8b5cf6;
|
||||||
|
--success: #10b981;
|
||||||
|
--warning: #f59e0b;
|
||||||
|
--danger: #ef4444;
|
||||||
|
|
||||||
|
/* Spacing */
|
||||||
|
--spacing-xs: 0.25rem;
|
||||||
|
--spacing-sm: 0.5rem;
|
||||||
|
--spacing-md: 1rem;
|
||||||
|
--spacing-lg: 1.5rem;
|
||||||
|
--spacing-xl: 2rem;
|
||||||
|
--spacing-2xl: 3rem;
|
||||||
|
|
||||||
|
/* Typography */
|
||||||
|
--font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||||
|
--font-mono: 'Fira Code', 'Monaco', 'Cascadia Code', monospace;
|
||||||
|
|
||||||
|
/* Border radius */
|
||||||
|
--radius-sm: 0.375rem;
|
||||||
|
--radius-md: 0.5rem;
|
||||||
|
--radius-lg: 0.75rem;
|
||||||
|
--radius-xl: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dark theme */
|
||||||
|
.dark {
|
||||||
|
--bg-primary: #0f172a;
|
||||||
|
--bg-secondary: #1e293b;
|
||||||
|
--bg-tertiary: #334155;
|
||||||
|
--text-primary: #f8fafc;
|
||||||
|
--text-secondary: #cbd5e1;
|
||||||
|
--text-muted: #94a3b8;
|
||||||
|
--border-color: #334155;
|
||||||
|
--border-hover: #475569;
|
||||||
|
--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.3);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.4);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reset and base styles */
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
scroll-behavior: smooth;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: var(--font-family);
|
||||||
|
line-height: 1.6;
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
transition: background-color 0.3s ease, color 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App layout */
|
||||||
|
.app {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation */
|
||||||
|
.nav {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--spacing-md) var(--spacing-xl);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 100;
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand h1 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-brand .version {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-decoration: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link.active {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link .icon {
|
||||||
|
font-size: 1.1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle {
|
||||||
|
background: none;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-toggle:hover {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Main content */
|
||||||
|
.main-content {
|
||||||
|
flex: 1;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Dashboard specific styles */
|
||||||
|
.dashboard {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Welcome section */
|
||||||
|
.welcome-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
background: linear-gradient(135deg, var(--accent-primary), var(--accent-secondary));
|
||||||
|
border-radius: var(--radius-xl);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content h1 {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
font-weight: 800;
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content p {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-indicators {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-dot {
|
||||||
|
width: 8px;
|
||||||
|
height: 8px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--success);
|
||||||
|
animation: pulse 2s infinite;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes pulse {
|
||||||
|
|
||||||
|
0%,
|
||||||
|
100% {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
opacity: 0.5;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sections */
|
||||||
|
.section-title {
|
||||||
|
font-size: 1.875rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Metrics section */
|
||||||
|
.metrics-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-header h3 {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-value {
|
||||||
|
font-size: 2.25rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-change {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 600;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-change.up {
|
||||||
|
color: var(--success);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-change.down {
|
||||||
|
color: var(--danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
.metric-change.neutral {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Feature demonstrations */
|
||||||
|
.features-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-demo {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-header h3 {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-tabs {
|
||||||
|
display: flex;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
padding: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab {
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tab.active {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-description {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.feature-content {
|
||||||
|
min-height: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-container,
|
||||||
|
.code-container {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.code-container {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
overflow-x: auto;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Component registry */
|
||||||
|
.components-section {
|
||||||
|
margin-top: var(--spacing-2xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
width: 300px;
|
||||||
|
transition: border-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-card {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-card:hover {
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
border-color: var(--border-hover);
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-header h4 {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-type {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.props-preview {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.prop-tag {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Demo specific styles */
|
||||||
|
.source-map-demo {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-element {
|
||||||
|
display: inline-block;
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-element:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-tree {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node {
|
||||||
|
margin: var(--spacing-sm) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.tree-node.root>.node-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.children {
|
||||||
|
margin-left: var(--spacing-lg);
|
||||||
|
border-left: 2px solid var(--border-color);
|
||||||
|
padding-left: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.node-label {
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.live-edit-demo {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-element {
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable-element:hover {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
justify-content: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-btn {
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 500;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-btn.primary {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-btn.secondary {
|
||||||
|
background: var(--accent-secondary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-btn.danger {
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.style-btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Footer */
|
||||||
|
.app-footer {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
text-align: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* App error state */
|
||||||
|
.app-error {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 50vh;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-error h1 {
|
||||||
|
color: var(--danger);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-error button {
|
||||||
|
margin-top: var(--spacing-lg);
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
cursor: pointer;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-error button:hover {
|
||||||
|
background: var(--accent-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive design */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.nav {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-links {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-section {
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.welcome-content h1 {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-header {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-input {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.metrics-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.components-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Design mode specific styles */
|
||||||
|
[data-design-mode-enabled="true"] [data-source-info] {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-design-mode-enabled="true"] [data-source-info]:hover {
|
||||||
|
outline: 2px dashed var(--accent-primary) !important;
|
||||||
|
outline-offset: 2px !important;
|
||||||
|
cursor: pointer !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Print styles */
|
||||||
|
@media print {
|
||||||
|
|
||||||
|
.nav,
|
||||||
|
.app-footer {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Interactive Demo Styles */
|
||||||
|
.interactive-demo {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.design-mode-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.design-mode-toggle.active {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.element-editor {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
animation: slideDown 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes slideDown {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-10px);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.editor-controls {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.control-group {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-workspace {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 300px;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
min-height: 500px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 2px dashed var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
position: relative;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-area.design-mode-active {
|
||||||
|
border-color: var(--accent-primary);
|
||||||
|
background: rgba(59, 130, 246, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
padding: var(--spacing-2xl);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
box-shadow: var(--shadow-sm);
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifications-panel {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-header {
|
||||||
|
padding: var(--spacing-md) var(--spacing-lg);
|
||||||
|
background: var(--bg-tertiary);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
color: var(--danger);
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:hover:not(:disabled) {
|
||||||
|
background: rgba(239, 68, 68, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.reset-btn:disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modifications-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modification-item {
|
||||||
|
background: var(--bg-primary);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.property {
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-values {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-change {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrow {
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.new-value {
|
||||||
|
color: var(--accent-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mod-time {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-footer {
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions ol {
|
||||||
|
margin-left: var(--spacing-xl);
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.instructions li {
|
||||||
|
margin-bottom: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Demo Elements */
|
||||||
|
.demo-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-description {
|
||||||
|
font-size: 1.125rem;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
max-width: 600px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button {
|
||||||
|
padding: var(--spacing-md) var(--spacing-xl);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
border: none;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button.primary {
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-button:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.demo-card {
|
||||||
|
background: white;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-md);
|
||||||
|
margin-top: var(--spacing-xl);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Selected state for design mode */
|
||||||
|
.selected {
|
||||||
|
outline: 2px solid var(--accent-primary) !important;
|
||||||
|
outline-offset: 2px;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected::after {
|
||||||
|
content: 'Selected';
|
||||||
|
position: absolute;
|
||||||
|
top: -24px;
|
||||||
|
left: 0;
|
||||||
|
background: var(--accent-primary);
|
||||||
|
color: white;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import { BrowserRouter } from 'react-router-dom';
|
||||||
|
import App from './App';
|
||||||
|
import './index.css';
|
||||||
|
import './index.css';
|
||||||
|
|
||||||
|
// 开发模式下的额外日志
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.log('🚀 Advanced AppDev Design Mode Example');
|
||||||
|
console.log('🔧 Plugin Configuration:', {
|
||||||
|
enabled: true,
|
||||||
|
prefix: 'design-mode',
|
||||||
|
verbose: true,
|
||||||
|
typescript: true,
|
||||||
|
react: true,
|
||||||
|
routing: true,
|
||||||
|
stateManagement: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 错误边界组件
|
||||||
|
class ErrorBoundary extends React.Component<
|
||||||
|
{ children: React.ReactNode },
|
||||||
|
{ hasError: boolean }
|
||||||
|
> {
|
||||||
|
constructor(props: { children: React.ReactNode }) {
|
||||||
|
super(props);
|
||||||
|
this.state = { hasError: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDerivedStateFromError(): { hasError: boolean } {
|
||||||
|
return { hasError: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||||
|
console.error('Error caught by boundary:', error, errorInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '2rem', textAlign: 'center', color: '#e53e3e' }}>
|
||||||
|
<h1>Something went wrong.</h1>
|
||||||
|
<p>Please refresh the page to try again.</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||||
|
<React.StrictMode>
|
||||||
|
<ErrorBoundary>
|
||||||
|
<BrowserRouter>
|
||||||
|
<App />
|
||||||
|
</BrowserRouter>
|
||||||
|
</ErrorBoundary>
|
||||||
|
</React.StrictMode>
|
||||||
|
);
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const ComponentShowcase: React.FC = () => {
|
||||||
|
const [activeCategory, setActiveCategory] = useState('buttons');
|
||||||
|
const [selectedComponent, setSelectedComponent] = useState<string | null>(null);
|
||||||
|
|
||||||
|
const categories = [
|
||||||
|
{ id: 'buttons', label: 'Buttons', icon: '🔘' },
|
||||||
|
{ id: 'forms', label: 'Forms', icon: '📝' },
|
||||||
|
{ id: 'cards', label: 'Cards', icon: '🃏' },
|
||||||
|
{ id: 'navigation', label: 'Navigation', icon: '🧭' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const components = {
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
id: 'primary-btn',
|
||||||
|
name: 'Primary Button',
|
||||||
|
description: 'Main action button with primary styling',
|
||||||
|
component: (
|
||||||
|
<button className="btn-primary">Primary Action</button>
|
||||||
|
),
|
||||||
|
code: `<button className="btn-primary">Primary Action</button>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'secondary-btn',
|
||||||
|
name: 'Secondary Button',
|
||||||
|
description: 'Secondary action button',
|
||||||
|
component: (
|
||||||
|
<button className="btn-secondary">Secondary Action</button>
|
||||||
|
),
|
||||||
|
code: `<button className="btn-secondary">Secondary Action</button>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'danger-btn',
|
||||||
|
name: 'Danger Button',
|
||||||
|
description: 'Destructive action button',
|
||||||
|
component: (
|
||||||
|
<button className="btn-danger">Delete Item</button>
|
||||||
|
),
|
||||||
|
code: `<button className="btn-danger">Delete Item</button>`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
forms: [
|
||||||
|
{
|
||||||
|
id: 'input-field',
|
||||||
|
name: 'Input Field',
|
||||||
|
description: 'Standard text input with label',
|
||||||
|
component: (
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" placeholder="Enter username" className="form-input" />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
code: `<div className="form-group">
|
||||||
|
<label>Username</label>
|
||||||
|
<input type="text" placeholder="Enter username" className="form-input" />
|
||||||
|
</div>`
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'checkbox-group',
|
||||||
|
name: 'Checkbox Group',
|
||||||
|
description: 'Multiple choice checkboxes',
|
||||||
|
component: (
|
||||||
|
<div className="form-group">
|
||||||
|
<label>Preferences</label>
|
||||||
|
<div className="checkbox-group">
|
||||||
|
<label><input type="checkbox" /> Email notifications</label>
|
||||||
|
<label><input type="checkbox" /> SMS alerts</label>
|
||||||
|
<label><input type="checkbox" /> Push notifications</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
code: `<div className="form-group">
|
||||||
|
<label>Preferences</label>
|
||||||
|
<div className="checkbox-group">
|
||||||
|
<label><input type="checkbox" /> Email notifications</label>
|
||||||
|
<label><input type="checkbox" /> SMS alerts</label>
|
||||||
|
<label><input type="checkbox" /> Push notifications</label>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
cards: [
|
||||||
|
{
|
||||||
|
id: 'info-card',
|
||||||
|
name: 'Info Card',
|
||||||
|
description: 'Card with title, content and actions',
|
||||||
|
component: (
|
||||||
|
<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3>Card Title</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<p>This is a sample card component with some descriptive content.</p>
|
||||||
|
</div>
|
||||||
|
<div className="card-actions">
|
||||||
|
<button className="btn-text">Learn More</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
code: `<div className="card">
|
||||||
|
<div className="card-header">
|
||||||
|
<h3>Card Title</h3>
|
||||||
|
</div>
|
||||||
|
<div className="card-body">
|
||||||
|
<p>This is a sample card component with some descriptive content.</p>
|
||||||
|
</div>
|
||||||
|
<div className="card-actions">
|
||||||
|
<button className="btn-text">Learn More</button>
|
||||||
|
</div>
|
||||||
|
</div>`
|
||||||
|
}
|
||||||
|
],
|
||||||
|
navigation: [
|
||||||
|
{
|
||||||
|
id: 'breadcrumb',
|
||||||
|
name: 'Breadcrumb',
|
||||||
|
description: 'Navigation breadcrumb trail',
|
||||||
|
component: (
|
||||||
|
<nav className="breadcrumb">
|
||||||
|
<a href="#">Home</a>
|
||||||
|
<span>/</span>
|
||||||
|
<a href="#">Products</a>
|
||||||
|
<span>/</span>
|
||||||
|
<span>Current Page</span>
|
||||||
|
</nav>
|
||||||
|
),
|
||||||
|
code: `<nav className="breadcrumb">
|
||||||
|
<a href="#">Home</a>
|
||||||
|
<span>/</span>
|
||||||
|
<a href="#">Products</a>
|
||||||
|
<span>/</span>
|
||||||
|
<span>Current Page</span>
|
||||||
|
</nav>`
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="component-showcase" data-page="showcase">
|
||||||
|
<header className="showcase-header">
|
||||||
|
<h1>Component Showcase</h1>
|
||||||
|
<p>Explore various UI components and their source mappings</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="showcase-content">
|
||||||
|
<aside className="category-sidebar">
|
||||||
|
<h3>Categories</h3>
|
||||||
|
<nav className="category-nav">
|
||||||
|
{categories.map(category => (
|
||||||
|
<button
|
||||||
|
key={category.id}
|
||||||
|
className={`category-btn ${activeCategory === category.id ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveCategory(category.id)}
|
||||||
|
data-category={category.id}
|
||||||
|
>
|
||||||
|
<span className="icon">{category.icon}</span>
|
||||||
|
<span className="label">{category.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main className="components-display">
|
||||||
|
<div className="components-grid">
|
||||||
|
{components[activeCategory as keyof typeof components]?.map(component => (
|
||||||
|
<div
|
||||||
|
key={component.id}
|
||||||
|
className={`component-item ${selectedComponent === component.id ? 'selected' : ''}`}
|
||||||
|
data-component={component.id}
|
||||||
|
onClick={() => setSelectedComponent(
|
||||||
|
selectedComponent === component.id ? null : component.id
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="component-preview" data-element="preview">
|
||||||
|
{component.component}
|
||||||
|
</div>
|
||||||
|
<div className="component-info">
|
||||||
|
<h4 data-element="name">{component.name}</h4>
|
||||||
|
<p data-element="description">{component.description}</p>
|
||||||
|
</div>
|
||||||
|
{selectedComponent === component.id && (
|
||||||
|
<div className="component-code" data-element="code">
|
||||||
|
<pre><code>{component.code}</code></pre>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ComponentShowcase;
|
||||||
@@ -0,0 +1,292 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
|
||||||
|
const ConfigurationGuide: React.FC = () => {
|
||||||
|
const [activeSection, setActiveSection] = useState('basic');
|
||||||
|
|
||||||
|
const sections = [
|
||||||
|
{ id: 'basic', label: 'Basic Setup', icon: '⚙️' },
|
||||||
|
{ id: 'options', label: 'Configuration Options', icon: '🔧' },
|
||||||
|
{ id: 'advanced', label: 'Advanced Usage', icon: '🚀' },
|
||||||
|
{ id: 'troubleshooting', label: 'Troubleshooting', icon: '🛠️' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const configExamples = {
|
||||||
|
basic: {
|
||||||
|
title: 'Basic Setup',
|
||||||
|
description: 'Get started with the plugin in minutes',
|
||||||
|
code: `// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import appdevDesignMode from 'vite-plugin-appdev-design-mode';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
appdevDesignMode() // Basic configuration
|
||||||
|
]
|
||||||
|
});`,
|
||||||
|
explanation: 'This is the minimal configuration needed to get started. The plugin will use all default options.'
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
title: 'Configuration Options',
|
||||||
|
description: 'Customize the plugin behavior with various options',
|
||||||
|
code: `// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import appdevDesignMode from 'vite-plugin-appdev-design-mode';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
appdevDesignMode({
|
||||||
|
// Enable/disable the plugin
|
||||||
|
enabled: true,
|
||||||
|
|
||||||
|
// Only enable in development
|
||||||
|
enableInProduction: false,
|
||||||
|
|
||||||
|
// Custom attribute prefix
|
||||||
|
attributePrefix: 'design-mode',
|
||||||
|
|
||||||
|
// Enable verbose logging
|
||||||
|
verbose: true,
|
||||||
|
|
||||||
|
// File patterns to exclude
|
||||||
|
exclude: [
|
||||||
|
'node_modules',
|
||||||
|
'.git',
|
||||||
|
'dist',
|
||||||
|
'**/*.test.{ts,tsx,js,jsx}'
|
||||||
|
],
|
||||||
|
|
||||||
|
// File patterns to include
|
||||||
|
include: [
|
||||||
|
'src/**/*.{ts,tsx,js,jsx}',
|
||||||
|
'components/**/*.{ts,tsx,js,jsx}'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
]
|
||||||
|
});`,
|
||||||
|
explanation: 'This configuration shows all available options and their default values.'
|
||||||
|
},
|
||||||
|
advanced: {
|
||||||
|
title: 'Advanced Usage',
|
||||||
|
description: 'Advanced patterns and integration techniques',
|
||||||
|
code: `// vite.config.ts
|
||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import appdevDesignMode from 'vite-plugin-appdev-design-mode';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
// Multiple configurations for different environments
|
||||||
|
appdevDesignMode({
|
||||||
|
enabled: true,
|
||||||
|
attributePrefix: 'dev-mode',
|
||||||
|
verbose: process.env.NODE_ENV === 'development',
|
||||||
|
|
||||||
|
// Conditional inclusion based on environment
|
||||||
|
include: process.env.NODE_ENV === 'development'
|
||||||
|
? ['src/**/*.{ts,tsx,js,jsx}']
|
||||||
|
: [], // Disable in production
|
||||||
|
|
||||||
|
// Custom exclude patterns
|
||||||
|
exclude: [
|
||||||
|
'node_modules',
|
||||||
|
'.git',
|
||||||
|
'dist',
|
||||||
|
'build',
|
||||||
|
'**/*.test.{ts,tsx,js,jsx}',
|
||||||
|
'**/__tests__/**',
|
||||||
|
'stories/**',
|
||||||
|
'**/*.stories.{ts,tsx}'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
],
|
||||||
|
|
||||||
|
// Server configuration for API endpoints
|
||||||
|
server: {
|
||||||
|
port: 5173,
|
||||||
|
host: true,
|
||||||
|
middlewareMode: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Environment-specific configuration
|
||||||
|
const isDevelopment = process.env.NODE_ENV === 'development';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: isDevelopment ? [
|
||||||
|
react(),
|
||||||
|
appdevDesignMode({
|
||||||
|
verbose: true,
|
||||||
|
attributePrefix: 'dev'
|
||||||
|
})
|
||||||
|
] : [react()]
|
||||||
|
});`,
|
||||||
|
explanation: 'This example shows how to configure the plugin differently based on environment and use cases.'
|
||||||
|
},
|
||||||
|
troubleshooting: {
|
||||||
|
title: 'Troubleshooting',
|
||||||
|
description: 'Common issues and their solutions',
|
||||||
|
code: `# Common Issues and Solutions
|
||||||
|
|
||||||
|
## Plugin not injecting source information
|
||||||
|
|
||||||
|
### Problem: Elements don't have data attributes
|
||||||
|
### Solution: Check file inclusion patterns
|
||||||
|
\`\`\`js
|
||||||
|
appdevDesignMode({
|
||||||
|
include: ['src/**/*.{ts,tsx,js,jsx}'] // Make sure this matches your project structure
|
||||||
|
})
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Performance issues
|
||||||
|
|
||||||
|
### Problem: Plugin slowing down development server
|
||||||
|
### Solution: Exclude unnecessary files
|
||||||
|
\`\`\`js
|
||||||
|
appdevDesignMode({
|
||||||
|
exclude: [
|
||||||
|
'node_modules',
|
||||||
|
'**/*.test.{ts,tsx,js,jsx}',
|
||||||
|
'**/__tests__/**',
|
||||||
|
'stories/**'
|
||||||
|
]
|
||||||
|
})
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## TypeScript errors
|
||||||
|
|
||||||
|
### Problem: Cannot find module '@babel/core'
|
||||||
|
### Solution: Install peer dependencies
|
||||||
|
\`\`\`bash
|
||||||
|
npm install --save-dev @babel/core @babel/traverse @babel/types
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## Custom configuration not working
|
||||||
|
|
||||||
|
### Problem: Plugin using default configuration
|
||||||
|
### Solution: Check import path and plugin options
|
||||||
|
\`\`\`js
|
||||||
|
// Correct import
|
||||||
|
import appdevDesignMode from 'vite-plugin-appdev-design-mode';
|
||||||
|
|
||||||
|
// Make sure options are passed correctly
|
||||||
|
appdevDesignMode({
|
||||||
|
enabled: true, // This should not be undefined
|
||||||
|
attributePrefix: 'custom'
|
||||||
|
})
|
||||||
|
\`\`\`
|
||||||
|
|
||||||
|
## API endpoints not accessible
|
||||||
|
|
||||||
|
### Problem: Cannot access plugin API endpoints
|
||||||
|
### Solution: Check server configuration and CORS
|
||||||
|
\`\`\`js
|
||||||
|
export default defineConfig({
|
||||||
|
server: {
|
||||||
|
cors: true, // Enable CORS for API access
|
||||||
|
headers: {
|
||||||
|
'Access-Control-Allow-Origin': '*'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
\`\`\``,
|
||||||
|
explanation: 'This section covers common problems developers encounter and their solutions.'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="config-guide" data-page="config">
|
||||||
|
<header className="guide-header">
|
||||||
|
<h1>Configuration Guide</h1>
|
||||||
|
<p>Learn how to configure and use the AppDev Design Mode plugin effectively</p>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="guide-content">
|
||||||
|
<nav className="guide-sidebar">
|
||||||
|
<h3>Sections</h3>
|
||||||
|
<ul className="guide-nav">
|
||||||
|
{Object.entries(configExamples).map(([key, section]) => (
|
||||||
|
<li key={key}>
|
||||||
|
<button
|
||||||
|
className={`nav-item ${activeSection === key ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveSection(key)}
|
||||||
|
data-section={key}
|
||||||
|
>
|
||||||
|
<span className="icon">{sections.find(s => s.id === key)?.icon}</span>
|
||||||
|
<span className="label">{section.title}</span>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main className="guide-main">
|
||||||
|
<div className="guide-section">
|
||||||
|
<div className="section-header">
|
||||||
|
<h2 data-element="section-title">
|
||||||
|
{configExamples[activeSection as keyof typeof configExamples].title}
|
||||||
|
</h2>
|
||||||
|
<p className="section-description" data-element="section-description">
|
||||||
|
{configExamples[activeSection as keyof typeof configExamples].description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="content-blocks">
|
||||||
|
<div className="code-block" data-element="code-block">
|
||||||
|
<div className="code-header">
|
||||||
|
<h4>Configuration</h4>
|
||||||
|
<button
|
||||||
|
className="copy-btn"
|
||||||
|
onClick={() => {
|
||||||
|
navigator.clipboard.writeText(configExamples[activeSection as keyof typeof configExamples].code);
|
||||||
|
}}
|
||||||
|
data-action="copy-code"
|
||||||
|
>
|
||||||
|
📋 Copy
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<pre className="code-content">
|
||||||
|
<code>{configExamples[activeSection as keyof typeof configExamples].code}</code>
|
||||||
|
</pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="explanation-block" data-element="explanation">
|
||||||
|
<h4>Explanation</h4>
|
||||||
|
<p>{configExamples[activeSection as keyof typeof configExamples].explanation}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="guide-footer">
|
||||||
|
<div className="additional-resources">
|
||||||
|
<h4>Additional Resources</h4>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a href="https://vitejs.dev/config/" target="_blank" rel="noopener noreferrer">
|
||||||
|
📖 Vite Configuration Documentation
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://babeljs.io/docs/babel-plugin-there/" target="_blank" rel="noopener noreferrer">
|
||||||
|
🔧 Babel Plugin API Documentation
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a href="https://github.com/vite-plugin-appdev-design-mode" target="_blank" rel="noopener noreferrer">
|
||||||
|
📦 Plugin Repository
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfigurationGuide;
|
||||||
@@ -0,0 +1,242 @@
|
|||||||
|
import React, { useState } from 'react';
|
||||||
|
import { useAppStore } from '../store/appStore';
|
||||||
|
|
||||||
|
interface MetricCardProps {
|
||||||
|
title: string;
|
||||||
|
value: string | number;
|
||||||
|
change?: string;
|
||||||
|
trend?: 'up' | 'down' | 'neutral';
|
||||||
|
icon: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const MetricCard: React.FC<MetricCardProps> = ({ title, value, change, trend, icon }) => (
|
||||||
|
<div className="metric-card" data-component="metric-card">
|
||||||
|
<div className="metric-header" data-element="metric-header">
|
||||||
|
<h3 data-element="metric-title">{title}</h3>
|
||||||
|
<span className="metric-icon" data-element="metric-icon">{icon}</span>
|
||||||
|
</div>
|
||||||
|
<div className="metric-value" data-element="metric-value">{value}</div>
|
||||||
|
{change && (
|
||||||
|
<div className={`metric-change ${trend}`} data-element="metric-change">
|
||||||
|
{trend === 'up' && '↗️'}
|
||||||
|
{trend === 'down' && '↘️'}
|
||||||
|
{trend === 'neutral' && '➡️'}
|
||||||
|
{change}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
interface FeatureDemoProps {
|
||||||
|
title: string;
|
||||||
|
description: string;
|
||||||
|
code: string;
|
||||||
|
demo: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeatureDemo: React.FC<FeatureDemoProps> = ({ title, description, code, demo }) => {
|
||||||
|
const [showCode, setShowCode] = useState(false);
|
||||||
|
const [activeTab, setActiveTab] = useState<'demo' | 'code'>('demo');
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="feature-demo" data-component="feature-demo">
|
||||||
|
<div className="feature-header" data-element="feature-header">
|
||||||
|
<h3 data-element="feature-title">{title}</h3>
|
||||||
|
<div className="feature-tabs" data-element="tabs">
|
||||||
|
<button
|
||||||
|
className={`tab ${activeTab === 'demo' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('demo')}
|
||||||
|
data-tab="demo"
|
||||||
|
>
|
||||||
|
Demo
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
className={`tab ${activeTab === 'code' ? 'active' : ''}`}
|
||||||
|
onClick={() => setActiveTab('code')}
|
||||||
|
data-tab="code"
|
||||||
|
>
|
||||||
|
Code
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p className="feature-description" data-element="description">{description}</p>
|
||||||
|
|
||||||
|
<div className="feature-content" data-element="content">
|
||||||
|
{activeTab === 'demo' ? (
|
||||||
|
<div className="demo-container" data-element="demo">
|
||||||
|
{demo}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<pre className="code-container" data-element="code">
|
||||||
|
<code>{code}</code>
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Dashboard: React.FC = () => {
|
||||||
|
const { theme, user, components } = useAppStore();
|
||||||
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
|
|
||||||
|
const metrics = [
|
||||||
|
{ title: 'Total Components', value: components.length, change: '+2', trend: 'up' as const, icon: '🧩' },
|
||||||
|
{ title: 'Active Elements', value: 24, change: '+5', trend: 'up' as const, icon: '⚡' },
|
||||||
|
{ title: 'Design Mode Sessions', value: 156, change: '+12', trend: 'up' as const, icon: '🎨' },
|
||||||
|
{ title: 'Code Quality Score', value: '94%', change: '+2%', trend: 'up' as const, icon: '📊' }
|
||||||
|
];
|
||||||
|
|
||||||
|
const componentList = components.filter(comp =>
|
||||||
|
comp.name.toLowerCase().includes(searchTerm.toLowerCase()) ||
|
||||||
|
comp.type.toLowerCase().includes(searchTerm.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="dashboard" data-page="dashboard">
|
||||||
|
{/* Welcome Section */}
|
||||||
|
<section className="welcome-section" data-section="welcome">
|
||||||
|
<div className="welcome-content" data-element="content">
|
||||||
|
<h1 data-element="title">
|
||||||
|
Welcome back, {user.name}!
|
||||||
|
</h1>
|
||||||
|
<p data-element="subtitle">
|
||||||
|
Here's what's happening with your design mode plugin today.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="status-indicators" data-element="indicators">
|
||||||
|
<div className="status-item" data-status="plugin-active">
|
||||||
|
<span className="status-dot active"></span>
|
||||||
|
<span>Plugin Active</span>
|
||||||
|
</div>
|
||||||
|
<div className="status-item" data-status="theme">
|
||||||
|
<span className="theme-indicator">{theme === 'light' ? '☀️' : '🌙'}</span>
|
||||||
|
<span>{theme.charAt(0).toUpperCase() + theme.slice(1)} Mode</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Metrics Grid */}
|
||||||
|
<section className="metrics-section" data-section="metrics">
|
||||||
|
<h2 className="section-title" data-element="section-title">System Metrics</h2>
|
||||||
|
<div className="metrics-grid">
|
||||||
|
{metrics.map((metric, index) => (
|
||||||
|
<MetricCard key={index} {...metric} />
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Feature Demonstrations */}
|
||||||
|
<section className="features-section" data-section="features">
|
||||||
|
<h2 className="section-title" data-element="section-title">Plugin Features</h2>
|
||||||
|
<div className="features-grid">
|
||||||
|
<FeatureDemo
|
||||||
|
title="Source Mapping"
|
||||||
|
description="Automatic injection of source location information into DOM elements during compilation."
|
||||||
|
code={`// 插件自动为JSX元素添加源码信息
|
||||||
|
<div className="example" data-source-info="...">
|
||||||
|
Content here
|
||||||
|
</div>`}
|
||||||
|
demo={
|
||||||
|
<div className="source-map-demo">
|
||||||
|
<div className="demo-element" data-element="mapped">
|
||||||
|
Hover over me to see source mapping info
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FeatureDemo
|
||||||
|
title="Component Tree"
|
||||||
|
description="Visual representation of component hierarchy with source locations."
|
||||||
|
code={`// 自动生成的组件树
|
||||||
|
App (App.tsx:12)
|
||||||
|
├── Header (Header.tsx:5)
|
||||||
|
├── Navigation (Nav.tsx:15)
|
||||||
|
└── MainContent (Main.tsx:8)`}
|
||||||
|
demo={
|
||||||
|
<div className="component-tree">
|
||||||
|
<div className="tree-node root" data-node="app">
|
||||||
|
<div className="node-label">App</div>
|
||||||
|
<div className="children">
|
||||||
|
<div className="tree-node" data-node="header">
|
||||||
|
<div className="node-label">Header</div>
|
||||||
|
</div>
|
||||||
|
<div className="tree-node" data-node="main">
|
||||||
|
<div className="node-label">MainContent</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<FeatureDemo
|
||||||
|
title="Live Editing"
|
||||||
|
description="Real-time style modifications with automatic HMR integration."
|
||||||
|
code={`// 修改样式实时生效
|
||||||
|
const modifyStyles = async (elementId, newStyles) => {
|
||||||
|
await fetch('/__modify_element_source', {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify({ elementId, newStyles })
|
||||||
|
});
|
||||||
|
};`}
|
||||||
|
demo={
|
||||||
|
<div className="live-edit-demo">
|
||||||
|
<div className="editable-element" data-editable="true">
|
||||||
|
Click to edit styles
|
||||||
|
</div>
|
||||||
|
<div className="style-controls">
|
||||||
|
<button className="style-btn primary">Primary</button>
|
||||||
|
<button className="style-btn secondary">Secondary</button>
|
||||||
|
<button className="style-btn danger">Danger</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Component Registry */}
|
||||||
|
<section className="components-section" data-section="components">
|
||||||
|
<div className="section-header" data-element="header">
|
||||||
|
<h2 className="section-title" data-element="title">Component Registry</h2>
|
||||||
|
<div className="search-container" data-element="search">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
placeholder="Search components..."
|
||||||
|
value={searchTerm}
|
||||||
|
onChange={(e) => setSearchTerm(e.target.value)}
|
||||||
|
className="search-input"
|
||||||
|
data-element="input"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="components-grid">
|
||||||
|
{componentList.map((component) => (
|
||||||
|
<div key={component.id} className="component-card" data-component={component.type}>
|
||||||
|
<div className="component-header">
|
||||||
|
<h4 data-element="name">{component.name}</h4>
|
||||||
|
<span className="component-type" data-element="type">{component.type}</span>
|
||||||
|
</div>
|
||||||
|
<div className="component-info" data-element="info">
|
||||||
|
<small>ID: {component.id}</small>
|
||||||
|
<div className="props-preview">
|
||||||
|
{Object.entries(component.props).slice(0, 2).map(([key, value]) => (
|
||||||
|
<span key={key} className="prop-tag">
|
||||||
|
{key}: {String(value)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Dashboard;
|
||||||
@@ -0,0 +1,74 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { DemoElement } from '../components/DemoElement';
|
||||||
|
|
||||||
|
const InteractiveDemo: React.FC = () => {
|
||||||
|
// No local state or context needed - Design Mode is handled globally by the plugin
|
||||||
|
|
||||||
|
const demoElements = [
|
||||||
|
{
|
||||||
|
id: 'hero-title',
|
||||||
|
tag: 'h1',
|
||||||
|
content: 'Interactive Design Mode Demo',
|
||||||
|
className: 'text-4xl font-bold text-slate-900 mb-4'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'hero-description',
|
||||||
|
tag: 'p',
|
||||||
|
content: 'Click on elements in design mode to modify their properties in real-time',
|
||||||
|
className: 'text-lg text-slate-600 mb-8 max-w-2xl'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'demo-button',
|
||||||
|
tag: 'button',
|
||||||
|
content: 'Click Me',
|
||||||
|
className: 'px-6 py-3 bg-blue-600 text-white rounded-lg font-semibold hover:bg-blue-700 transition-colors shadow-sm'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'demo-card',
|
||||||
|
tag: 'div',
|
||||||
|
content: 'Sample Card Content',
|
||||||
|
className: 'mt-8 p-6 bg-white rounded-xl shadow-md border border-slate-200'
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-8" data-page="interactive">
|
||||||
|
<header className="flex flex-col gap-6 bg-slate-50 p-8 rounded-xl border border-slate-200">
|
||||||
|
<div className="flex justify-between items-center">
|
||||||
|
<h1 className="text-2xl font-bold text-slate-900">Interactive Design Mode</h1>
|
||||||
|
<p className="text-sm text-slate-500">
|
||||||
|
Design Mode is now injected globally. Use the floating toggle in the bottom right to enable it.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 gap-8 min-h-[600px]">
|
||||||
|
<div className="bg-slate-50 border-2 border-dashed border-slate-300 rounded-xl p-8 relative">
|
||||||
|
<div className="bg-white p-12 rounded-lg shadow-sm min-h-full">
|
||||||
|
{demoElements.map(element => (
|
||||||
|
<DemoElement
|
||||||
|
key={element.id}
|
||||||
|
element={element}
|
||||||
|
isDesignMode={false} // The global manager handles selection visuals
|
||||||
|
onSelect={() => {}} // Handled globally
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer className="bg-slate-50 p-6 rounded-xl border border-slate-200">
|
||||||
|
<div className="max-w-3xl">
|
||||||
|
<h4 className="font-semibold text-slate-900 mb-4">Instructions:</h4>
|
||||||
|
<ol className="list-decimal list-inside space-y-2 text-slate-600 text-sm">
|
||||||
|
<li>Toggle <strong>Design Mode</strong> using the floating button</li>
|
||||||
|
<li>Click on any element to select and edit it</li>
|
||||||
|
<li>Modifications are applied globally</li>
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default InteractiveDemo;
|
||||||
@@ -0,0 +1,173 @@
|
|||||||
|
import { create } from 'zustand';
|
||||||
|
import { devtools, persist } from 'zustand/middleware';
|
||||||
|
|
||||||
|
interface AppState {
|
||||||
|
// UI状态
|
||||||
|
theme: 'light' | 'dark';
|
||||||
|
isLoading: boolean;
|
||||||
|
error: string | null;
|
||||||
|
|
||||||
|
// 应用状态
|
||||||
|
currentPage: string;
|
||||||
|
user: {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
preferences: {
|
||||||
|
showTooltips: boolean;
|
||||||
|
animations: boolean;
|
||||||
|
compactMode: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
// 组件状态
|
||||||
|
components: Array<{
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
props: Record<string, any>;
|
||||||
|
children?: string[];
|
||||||
|
parent?: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
// 设计模式状态
|
||||||
|
designMode: {
|
||||||
|
enabled: boolean;
|
||||||
|
selectedElement: string | null;
|
||||||
|
showGrid: boolean;
|
||||||
|
showInfo: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Actions
|
||||||
|
setTheme: (theme: 'light' | 'dark') => void;
|
||||||
|
toggleTheme: () => void;
|
||||||
|
setLoading: (loading: boolean) => void;
|
||||||
|
setError: (error: string | null) => void;
|
||||||
|
setCurrentPage: (page: string) => void;
|
||||||
|
updateUserPreferences: (preferences: Partial<AppState['user']['preferences']>) => void;
|
||||||
|
|
||||||
|
// 组件操作
|
||||||
|
addComponent: (component: Omit<AppState['components'][0], 'id'>) => void;
|
||||||
|
updateComponent: (id: string, updates: Partial<AppState['components'][0]>) => void;
|
||||||
|
removeComponent: (id: string) => void;
|
||||||
|
|
||||||
|
// 设计模式操作
|
||||||
|
setDesignModeEnabled: (enabled: boolean) => void;
|
||||||
|
setSelectedElement: (element: string | null) => void;
|
||||||
|
toggleGrid: () => void;
|
||||||
|
toggleInfo: () => void;
|
||||||
|
|
||||||
|
// 重置状态
|
||||||
|
reset: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState = {
|
||||||
|
theme: 'light' as const,
|
||||||
|
isLoading: false,
|
||||||
|
error: null,
|
||||||
|
currentPage: 'dashboard',
|
||||||
|
user: {
|
||||||
|
name: 'Demo User',
|
||||||
|
email: 'demo@example.com',
|
||||||
|
preferences: {
|
||||||
|
showTooltips: true,
|
||||||
|
animations: true,
|
||||||
|
compactMode: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: [
|
||||||
|
{
|
||||||
|
id: 'header-1',
|
||||||
|
name: 'AppHeader',
|
||||||
|
type: 'header',
|
||||||
|
props: { title: 'Advanced Demo', showNav: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'card-1',
|
||||||
|
name: 'InfoCard',
|
||||||
|
type: 'card',
|
||||||
|
props: { title: 'Welcome', content: 'This is an advanced example' }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
designMode: {
|
||||||
|
enabled: false,
|
||||||
|
selectedElement: null,
|
||||||
|
showGrid: false,
|
||||||
|
showInfo: true
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useAppStore = create<AppState>()(
|
||||||
|
devtools(
|
||||||
|
persist(
|
||||||
|
(set, get) => ({
|
||||||
|
...initialState,
|
||||||
|
|
||||||
|
// UI Actions
|
||||||
|
setTheme: (theme) => set({ theme }, false, 'setTheme'),
|
||||||
|
toggleTheme: () => set((state) => ({
|
||||||
|
theme: state.theme === 'light' ? 'dark' : 'light'
|
||||||
|
}), false, 'toggleTheme'),
|
||||||
|
|
||||||
|
setLoading: (loading) => set({ isLoading: loading }, false, 'setLoading'),
|
||||||
|
setError: (error) => set({ error }, false, 'setError'),
|
||||||
|
setCurrentPage: (page) => set({ currentPage: page }, false, 'setCurrentPage'),
|
||||||
|
|
||||||
|
updateUserPreferences: (preferences) => set((state) => ({
|
||||||
|
user: {
|
||||||
|
...state.user,
|
||||||
|
preferences: { ...state.user.preferences, ...preferences }
|
||||||
|
}
|
||||||
|
}), false, 'updateUserPreferences'),
|
||||||
|
|
||||||
|
// Component Actions
|
||||||
|
addComponent: (component) => set((state) => ({
|
||||||
|
components: [
|
||||||
|
...state.components,
|
||||||
|
{ ...component, id: `${component.name}-${Date.now()}` }
|
||||||
|
]
|
||||||
|
}), false, 'addComponent'),
|
||||||
|
|
||||||
|
updateComponent: (id, updates) => set((state) => ({
|
||||||
|
components: state.components.map(comp =>
|
||||||
|
comp.id === id ? { ...comp, ...updates } : comp
|
||||||
|
)
|
||||||
|
}), false, 'updateComponent'),
|
||||||
|
|
||||||
|
removeComponent: (id) => set((state) => ({
|
||||||
|
components: state.components.filter(comp => comp.id !== id)
|
||||||
|
}), false, 'removeComponent'),
|
||||||
|
|
||||||
|
// Design Mode Actions
|
||||||
|
setDesignModeEnabled: (enabled) => set((state) => ({
|
||||||
|
designMode: { ...state.designMode, enabled }
|
||||||
|
}), false, 'setDesignModeEnabled'),
|
||||||
|
|
||||||
|
setSelectedElement: (element) => set((state) => ({
|
||||||
|
designMode: { ...state.designMode, selectedElement: element }
|
||||||
|
}), false, 'setSelectedElement'),
|
||||||
|
|
||||||
|
toggleGrid: () => set((state) => ({
|
||||||
|
designMode: { ...state.designMode, showGrid: !state.designMode.showGrid }
|
||||||
|
}), false, 'toggleGrid'),
|
||||||
|
|
||||||
|
toggleInfo: () => set((state) => ({
|
||||||
|
designMode: { ...state.designMode, showInfo: !state.designMode.showInfo }
|
||||||
|
}), false, 'toggleInfo'),
|
||||||
|
|
||||||
|
// Reset
|
||||||
|
reset: () => set(initialState, false, 'reset')
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
name: 'advanced-design-mode-storage',
|
||||||
|
partialize: (state) => ({
|
||||||
|
theme: state.theme,
|
||||||
|
user: state.user,
|
||||||
|
designMode: state.designMode
|
||||||
|
})
|
||||||
|
}
|
||||||
|
),
|
||||||
|
{
|
||||||
|
name: 'app-store'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import { type ClassValue, clsx } from "clsx";
|
||||||
|
import { twMerge } from "tailwind-merge";
|
||||||
|
|
||||||
|
export function cn(...inputs: ClassValue[]) {
|
||||||
|
return twMerge(clsx(inputs));
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
/** @type {import('tailwindcss').Config} */
|
||||||
|
export default {
|
||||||
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{js,ts,jsx,tsx}",
|
||||||
|
],
|
||||||
|
theme: {
|
||||||
|
extend: {},
|
||||||
|
},
|
||||||
|
plugins: [],
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"useDefineForClassFields": true,
|
||||||
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
|
"module": "ESNext",
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowImportingTsExtensions": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"noEmit": true,
|
||||||
|
"jsx": "react-jsx",
|
||||||
|
"strict": true,
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true,
|
||||||
|
"baseUrl": ".",
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["src/*"],
|
||||||
|
"@components/*": ["src/components/*"],
|
||||||
|
"@pages/*": ["src/pages/*"],
|
||||||
|
"@utils/*": ["src/utils/*"],
|
||||||
|
"@types/*": ["src/types/*"],
|
||||||
|
"@hooks/*": ["src/hooks/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src"],
|
||||||
|
"references": [{ "path": "./tsconfig.node.json" }]
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"composite": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"allowSyntheticDefaultImports": true
|
||||||
|
},
|
||||||
|
"include": ["vite.config.ts"]
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import react from '@vitejs/plugin-react';
|
||||||
|
import appdevDesignMode from '../../packages/plugin/src/index';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [
|
||||||
|
react(),
|
||||||
|
// 高级配置示例
|
||||||
|
appdevDesignMode({
|
||||||
|
enabled: true,
|
||||||
|
enableInProduction: false, // 只在开发环境启用
|
||||||
|
attributePrefix: 'design-mode', // 自定义属性前缀
|
||||||
|
verbose: true, // 详细日志输出
|
||||||
|
exclude: [
|
||||||
|
'node_modules',
|
||||||
|
'.git',
|
||||||
|
'dist',
|
||||||
|
'build',
|
||||||
|
'**/*.test.{ts,tsx,js,jsx}',
|
||||||
|
'**/*.spec.{ts,tsx,js,jsx}',
|
||||||
|
'**/__tests__/**',
|
||||||
|
'**/tests/**',
|
||||||
|
],
|
||||||
|
include: [
|
||||||
|
'src/**/*.{ts,tsx,js,jsx}',
|
||||||
|
'components/**/*.{ts,tsx,js,jsx}',
|
||||||
|
'pages/**/*.{ts,tsx,js,jsx}',
|
||||||
|
'!**/node_modules/**',
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
|
||||||
|
// 高级Vite配置
|
||||||
|
server: {
|
||||||
|
port: 5174, // 使用不同端口避免冲突
|
||||||
|
host: true,
|
||||||
|
open: true,
|
||||||
|
cors: true,
|
||||||
|
fs: {
|
||||||
|
allow: ['..', '../../src'], // Allow serving the plugin source
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
build: {
|
||||||
|
outDir: 'dist',
|
||||||
|
sourcemap: true,
|
||||||
|
minify: 'esbuild',
|
||||||
|
rollupOptions: {
|
||||||
|
output: {
|
||||||
|
manualChunks: {
|
||||||
|
vendor: ['react', 'react-dom'],
|
||||||
|
router: ['react-router-dom'],
|
||||||
|
state: ['zustand'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': '/src',
|
||||||
|
'@components': '/src/components',
|
||||||
|
'@pages': '/src/pages',
|
||||||
|
'@utils': '/src/utils',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
css: {
|
||||||
|
modules: {
|
||||||
|
localsConvention: 'camelCaseOnly',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
optimizeDeps: {
|
||||||
|
include: ['react', 'react-dom', 'react-router-dom', 'zustand'],
|
||||||
|
},
|
||||||
|
});
|
||||||
78
qiming-vite-plugin-design-mode/examples/basic/index.html
Normal file
78
qiming-vite-plugin-design-mode/examples/basic/index.html
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Basic AppDev Design Mode Example</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', sans-serif;
|
||||||
|
margin: 0;
|
||||||
|
padding: 2rem;
|
||||||
|
background: linear-gradient(135deg, #74b9ff, #0984e3);
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
.container {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 0 auto;
|
||||||
|
background: white;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 2rem;
|
||||||
|
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
|
||||||
|
}
|
||||||
|
h1 { color: #2d3748; }
|
||||||
|
.example-box {
|
||||||
|
background: #f7fafc;
|
||||||
|
border: 2px solid #e2e8f0;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 1rem;
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
.button {
|
||||||
|
background: #4299e1;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 5px;
|
||||||
|
cursor: pointer;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
.button:hover {
|
||||||
|
background: #3182ce;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="container">
|
||||||
|
<h1 data-appdev-element="main-title">Basic Design Mode Example</h1>
|
||||||
|
<p data-appdev-element="description">
|
||||||
|
This is a simple HTML example showing how the design mode plugin works.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="example-box" data-appdev-component="interactive-box">
|
||||||
|
<h2 data-appdev-element="box-title">Interactive Elements</h2>
|
||||||
|
<p data-appdev-element="box-description">
|
||||||
|
Click the buttons below to test the design mode functionality.
|
||||||
|
</p>
|
||||||
|
<button class="button" data-appdev-action="primary">Primary Button</button>
|
||||||
|
<button class="button" data-appdev-action="secondary">Secondary Button</button>
|
||||||
|
<button class="button" data-appdev-action="danger">Danger Button</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="example-box" data-appdev-component="content-section">
|
||||||
|
<h3 data-appdev-element="content-title">Content Section</h3>
|
||||||
|
<p data-appdev-element="content-text">
|
||||||
|
This section contains various HTML elements for testing source mapping.
|
||||||
|
</p>
|
||||||
|
<ul data-appdev-element="feature-list">
|
||||||
|
<li data-appdev-item="feature-1">Source mapping works with vanilla HTML</li>
|
||||||
|
<li data-appdev-item="feature-2">Plugin adds data attributes automatically</li>
|
||||||
|
<li data-appdev-item="feature-3">No React or framework required</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module" src="/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user