"添加前端模板和运行代码模块"

This commit is contained in:
Codex
2026-06-01 13:43:09 +08:00
parent 24045274ad
commit 8092c4b1f8
587 changed files with 99761 additions and 0 deletions

3
.gitignore vendored
View File

@@ -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/

View 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. 如果存在,直接使用缓存;否则处理代码并缓存结果

View 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
```

View 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);
}
}
```

View 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!"
```

View 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"}'
```

View 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
```

View 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"}'
```

View 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", &params_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", &params_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字符串。

View 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
View File

@@ -0,0 +1,6 @@
/target
.idea
.vscode
.DS_Store
temp/

View 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

File diff suppressed because it is too large Load Diff

View 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"]

View 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

View 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.

View 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.

View 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.

View 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.

View File

@@ -0,0 +1,335 @@
# MCP 代码执行工具
一个基于 Rust 的工具,提供命令行界面和 MCPModel 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` 文件。

View File

@@ -0,0 +1,6 @@
{
"name": "Script Runner",
"description": "执行JavaScript、TypeScript和Python代码",
"path": "/path/to/script_runner",
"transport": "stdio"
}

View 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');

View 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来调用我们的服务

View 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

View 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,
};
}

View 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()
};
}

View 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
};
}

View 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
};
}

View 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
}
};
}

View 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
};
}

View 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()
};
}

View 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',
};
}

View 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

View 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,
};
}

View 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

View 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}`;
}

View 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 };
}

View 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} 的结果`
};
}

View 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}")

View 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))

View 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
}

View 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) + " 的结果"
}

View 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
}

View 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 "未知类型"

View 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}`;
}

View 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} 的结果`
};
}

View 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),
}

View 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
View File

@@ -0,0 +1,3 @@
mod code_file_cache;
pub use code_file_cache::CodeFileCache;

View 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(&params)?;
// 创建临时文件写入参数
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
}

View 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())
}
}

View File

@@ -0,0 +1,6 @@
mod common_runner;
mod js_runner;
mod ts_runner;
pub use js_runner::JsRunner;
pub use ts_runner::TsRunner;

View 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())
}
}

View 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;

View 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}");
}
}

View 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()),
}
}
}

View File

@@ -0,0 +1,3 @@
mod mcp_server;
pub use mcp_server::{CodeRunnerService,CodeRunRequest};

View 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,
},
}
}
}

View 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;

View 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>,
}

View 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 }

View File

@@ -0,0 +1,3 @@
mod python_dependencies;
pub use python_dependencies::*;

View File

@@ -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(())
}
}

View File

@@ -0,0 +1,5 @@
mod dependencies;
mod python_runner;
pub use dependencies::parse_import;
pub use python_runner::PythonRunner;

View 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(&params)?;
// 创建临时文件写入参数
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)
}
}

View 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(())
}
}

View 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()
}));
}
})();

View 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()
}));
}
})();

View 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
}))

View 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();

View 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(&params).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(())
}
}

View 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;

View 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(&params).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(())
}
}

View 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(())
}
}

View 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();
}
}

View 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

View 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/

View 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`.

View 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` 均校验目标路径必须位于项目根目录内,防止路径穿越。

View 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

View 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
```

View 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>

File diff suppressed because it is too large Load Diff

View File

@@ -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"
}
}

View File

@@ -0,0 +1,6 @@
export default {
plugins: {
'@tailwindcss/postcss': {},
autoprefixer: {},
},
}

View 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;

View File

@@ -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>
);
};

View 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;
}

View File

@@ -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>
);

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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'
}
)
);

View File

@@ -0,0 +1,6 @@
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}

View File

@@ -0,0 +1,12 @@
/** @type {import('tailwindcss').Config} */
export default {
content: [
"./index.html",
"./src/**/*.{js,ts,jsx,tsx}",
],
theme: {
extend: {},
},
plugins: [],
}

View File

@@ -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" }]
}

View File

@@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

View File

@@ -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'],
},
});

View 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