- Drop the now-stale "`yarn logs`" from the 技术栈 table; the service
no longer shell-outs the yarn CLI, all cluster calls go via httpx
(yarn_client.py).
- New "RM / SHS API 端点" section between 提交流程 and 配置项:
- RM: 4 endpoints (/ws/v1/cluster/apps, /{appid},
/{appid}/aggregated-logs, PUT /{appid}/state) with the
triggering tool for each.
- Documents the amContainerLogs 3xx-follow fallback (manual,
3-hop max) and why (httpx drops Authorization across hosts).
- SHS: explicitly notes zero built-in calls; history_server_url
is stored-for-future, today SHS access goes through fetch_url
+ url_allowlist only.
513 lines
17 KiB
Markdown
513 lines
17 KiB
Markdown
# spark-executor-mcp
|
||
|
||
`spark-executor-mcp` 是一个基于 **FastAPI + fastapi-mcp** 的 Python MCP 服务集合,主要用于让 LLM Agent 以受控方式操作 Spark-on-YARN:保存集群连接、写入或选择 PySpark 脚本、准备提交、二次确认提交、查询状态、读取日志和终止作业。
|
||
|
||
项目同时内置一个受沙箱限制的 `files_mcp` 服务,用于在指定目录内执行文件读写、查询、移动、复制等操作。
|
||
|
||
## 核心能力
|
||
|
||
### Spark Executor MCP
|
||
|
||
- 管理 Spark/YARN 连接配置。
|
||
- 将 LLM 生成的 PySpark 代码写入服务端文件系统。
|
||
- 以 **两步提交** 方式运行 Spark 作业:
|
||
1. `prepare_submit_job`:只生成待确认提交记录,不执行 `spark-submit`。
|
||
2. `confirm_submit_job`:用户二次确认后才真正调用 `spark-submit`。
|
||
- 查询 Spark/YARN 作业状态、终态结果和聚合日志。
|
||
- 通过本地 `job_id` 或 YARN `application_id` 查询同一个作业。
|
||
- Kill 正在运行的 YARN application。
|
||
- 持久化连接、待提交作业和已提交作业记录。
|
||
|
||
### Files MCP
|
||
|
||
- 在 `FILES_MCP_ROOT` 指定的沙箱目录内创建、读取、更新、删除文件。
|
||
- 支持目录列举、路径状态查询、glob 搜索、移动和复制。
|
||
- 路径会经过沙箱校验,避免访问沙箱外文件。
|
||
- 文本内容限制为 UTF-8,单文件读写默认有大小上限。
|
||
|
||
### 可插拔 MCP 服务
|
||
|
||
- 根应用启动时会自动发现并挂载内置 MCP 服务。
|
||
- 可通过 `MCP_SERVICES` 环境变量选择启用哪些服务。
|
||
- 新增 MCP 服务只需要提供一个 FastAPI 子应用和 `SERVICE: McpService` 声明。
|
||
|
||
## 技术栈
|
||
|
||
| 类别 | 技术 |
|
||
| --- | --- |
|
||
| 语言 | Python 3.12+ |
|
||
| Web 框架 | FastAPI |
|
||
| MCP 暴露 | fastapi-mcp |
|
||
| 数据模型 | Pydantic v2 |
|
||
| HTTP 客户端 | httpx、httpx-kerberos |
|
||
| Spark 集成 | `spark-submit`、YARN REST API (httpx 直连) |
|
||
| 日志 | loguru |
|
||
| 进程管理 | uvicorn、gunicorn |
|
||
| 包管理 | uv |
|
||
| 测试 | pytest、pytest-mock |
|
||
| 容器 | Docker、Docker Compose |
|
||
|
||
## 快速开始
|
||
|
||
### 1. 安装依赖
|
||
|
||
```bash
|
||
uv sync
|
||
```
|
||
|
||
项目依赖在 `pyproject.toml` 和 `uv.lock` 中固定,默认 PyPI 镜像配置为清华源。
|
||
|
||
### 2. 启动服务
|
||
|
||
开发环境直接运行:
|
||
|
||
```bash
|
||
uv run main.py
|
||
```
|
||
|
||
默认监听:
|
||
|
||
```text
|
||
0.0.0.0:8000
|
||
```
|
||
|
||
健康检查:
|
||
|
||
```bash
|
||
curl http://localhost:8000/health
|
||
```
|
||
|
||
预期返回:
|
||
|
||
```json
|
||
{"status":"ok"}
|
||
```
|
||
|
||
### 3. MCP 端点
|
||
|
||
服务启动后会挂载内置 MCP 服务:
|
||
|
||
| 服务 | MCP endpoint | 说明 |
|
||
| --- | --- | --- |
|
||
| Spark Executor | `http://localhost:8000/spark-executor-mcp` | Spark-on-YARN 作业管理 |
|
||
| Files MCP | `http://localhost:8000/files-mcp` | 沙箱文件系统工具 |
|
||
|
||
MCP 客户端需要先执行 `initialize` 握手,拿到 `mcp-session-id` 后,再调用 `tools/list` 和 `tools/call`。不要跳过握手。
|
||
|
||
## MCP 工具概览
|
||
|
||
### Spark Executor 工具
|
||
|
||
| 工具 | 作用 |
|
||
| --- | --- |
|
||
| `save_connection` | 保存或更新一个 Spark/YARN 连接配置 |
|
||
| `list_connections` | 列出所有连接配置 |
|
||
| `get_connection` | 按名称读取连接配置 |
|
||
| `delete_connection` | 删除连接配置 |
|
||
| `update_connection` | 部分更新一个已存在的连接 (PATCH 语义, 只改提供的字段) |
|
||
| `write_job_file` | 将 LLM 已生成的 PySpark 代码写入服务端文件 |
|
||
| `read_job_file` | 读取已存在的 PySpark 脚本内容 |
|
||
| `update_job_file` | 覆盖更新已存在的 PySpark 脚本 |
|
||
| `prepare_submit_job` | 创建待确认提交记录,不执行 `spark-submit` |
|
||
| `update_pending_job` | 在提交前更新待确认作业参数 |
|
||
| `list_pending_jobs` | 列出待提交/已提交/已取消/失败的 pending 记录 |
|
||
| `get_pending_job` | 查看单个 pending 记录 |
|
||
| `cancel_pending_job` | 取消还未提交的 pending 记录 |
|
||
| `confirm_submit_job` | 真正执行 `spark-submit` |
|
||
| `get_job_status` | 查询 YARN 当前状态 |
|
||
| `get_job_result` | 查询终态结果视图 |
|
||
| `get_job_logs` | 拉取 YARN 聚合日志 |
|
||
| `kill_job` | Kill YARN application |
|
||
| `get_external_job_status` | 查询**非本服务提交**的外部 YARN application 状态(按 `application_id` + `connection_name`) |
|
||
| `get_external_job_result` | 查询外部 YARN application 终态结果视图 |
|
||
| `get_external_job_logs` | 拉取外部 YARN application 的聚合日志 |
|
||
| `list_applications` | 列出 YARN 上所有应用(按 `state` / `queue` / `limit` 过滤),绕过 JobStore |
|
||
| `fetch_url` | 代理 HTTP GET 到集群内网 URL (host 受 `Connection.url_allowlist` glob allowlist 约束, 空则全拒) |
|
||
|
||
### Files MCP 工具
|
||
|
||
| 工具 | 作用 |
|
||
| --- | --- |
|
||
| `create_file` | 在沙箱内创建文本文件 |
|
||
| `read_file` | 读取沙箱内文本文件 |
|
||
| `update_file` | 覆盖更新文本文件 |
|
||
| `delete_file` | 删除文件或目录 |
|
||
| `list_dir` | 列出目录内容 |
|
||
| `stat` | 查看路径类型、大小、mtime、权限等信息 |
|
||
| `search` | 使用 glob pattern 搜索路径 |
|
||
| `move_file` | 移动或重命名文件/目录 |
|
||
| `copy_file` | 复制文件/目录 |
|
||
|
||
## Spark 作业提交流程
|
||
|
||
Spark 作业提交是故意设计成两步,不要绕过。
|
||
|
||
### 1. 保存连接
|
||
|
||
先保存一个连接配置,后续提交作业时通过 `connection` 名称引用它。
|
||
|
||
示例参数:
|
||
|
||
```json
|
||
{
|
||
"name": "prod-yarn",
|
||
"master": "yarn",
|
||
"deploy_mode": "cluster",
|
||
"yarn_rm_url": "http://yarn-rm.example.com:8088",
|
||
"spark_conf": {
|
||
"spark.yarn.queue": "default"
|
||
}
|
||
}
|
||
```
|
||
|
||
调用工具:
|
||
|
||
```text
|
||
save_connection
|
||
```
|
||
|
||
### 2. 写入或选择 PySpark 脚本
|
||
|
||
如果脚本由 LLM 生成,必须先调用:
|
||
|
||
```text
|
||
write_job_file
|
||
```
|
||
|
||
该工具会把代码写入 `SPARK_EXECUTOR_JOBS_DIR`,并返回真实存在于服务端文件系统中的 `script_path`。
|
||
|
||
如果脚本已经存在,需要确保传入的是容器或服务进程能访问到的真实路径。只存在于 Agent 上下文里的“虚构路径”会被拒绝。
|
||
|
||
### 3. 准备提交
|
||
|
||
调用:
|
||
|
||
```text
|
||
prepare_submit_job
|
||
```
|
||
|
||
这个步骤会:
|
||
|
||
- 读取并快照指定 `Connection` 的 `master`、`deploy_mode`、`spark_conf`、`yarn_rm_url`。
|
||
- 创建 `PendingSubmission` 持久化记录。
|
||
- 返回 `pending_id`。
|
||
- **不会调用 `spark-submit`。**
|
||
|
||
注意:如果准备后再修改连接配置,已经创建的 pending job 不会被重新定向到新连接。快照就是快照,别让状态到处乱飞。
|
||
|
||
### 4. 二次确认提交
|
||
|
||
用户确认后调用:
|
||
|
||
```text
|
||
confirm_submit_job
|
||
```
|
||
|
||
参数只需要 `pending_id`。该步骤才会真正调用 `spark-submit`,成功后会创建 `Job` 记录,并返回本地 `job_id` 和 YARN `application_id`。
|
||
|
||
### 5. 查询、读日志或终止
|
||
|
||
后续工具的 `job_id` 参数同时支持:
|
||
|
||
- 本地 12 位十六进制 `job_id`。
|
||
- YARN `application_id`,例如 `application_17400000001_0001`。
|
||
|
||
可用工具:
|
||
|
||
```text
|
||
get_job_status
|
||
get_job_result
|
||
get_job_logs
|
||
kill_job
|
||
```
|
||
|
||
## RM / SHS API 端点
|
||
|
||
本服务**不再 shell-out `yarn` CLI**,所有集群交互都通过 `httpx` 直接打 REST(由 `spark_executor/core/yarn_client.py` 统一封装)。鉴权走 `Connection.auth_type`(`none` / `simple` / `basic` / `kerberos` + SPNEGO),SSL 走 `Connection.ssl_verify` / `ssl_ca_bundle`。
|
||
|
||
### YARN ResourceManager
|
||
|
||
| 方法 | 端点 | 用途 |
|
||
| --- | --- | --- |
|
||
| `GET` | `/ws/v1/cluster/apps?state=&queue=&limit=` | 列出 YARN 应用(支持 `state` / `queue` / `limit` 过滤),由 `list_applications` 触发 |
|
||
| `GET` | `/ws/v1/cluster/apps/{appid}` | 取单个 app 的 state + amContainerLogs;日志降级路径会复用 |
|
||
| `GET` | `/ws/v1/cluster/apps/{appid}/aggregated-logs` | 拉聚合后的 container 日志,404/501 时自动降级到 amContainerLogs 路径 |
|
||
| `PUT` | `/ws/v1/cluster/apps/{appid}/state`,body `{"state":"KILLED"}` | 杀应用,由 `kill_job` 触发 |
|
||
|
||
> **日志降级路径**会**手动**跟随 3xx Location 跳到 NodeManager(最多 3 次)抓 AM driver 的 `/stdout`。原因:httpx 默认跨主机重定向会丢掉 `Authorization`,RM→NM 的 307 在很多集群会因此 401/403。driver 日志可以这样取,**executor 日志仍然依赖 `yarn.log-aggregation-enable=true`**,这个不走降级。
|
||
|
||
### Spark History Server
|
||
|
||
**当前没有任何内置调用。** `Connection.history_server_url` 字段只是原样存储,留作未来 SHS 专用工具消费(`models.py` 的 docstring 明确写明)。
|
||
|
||
要走 SHS API(典型路径 `/api/v1/applications/{id}/jobs`、`/api/v1/applications/{id}/stages` 等),**唯一**方式是用 `fetch_url` 工具:
|
||
|
||
1. 把 SHS 主机 glob 加到对应 `Connection.url_allowlist`(单标签主机如 `ccam*`,或多级域名如 `*.hadoop.internal`)。
|
||
2. 调用 `fetch_url` 时传入完整 SHS URL,鉴权和 SSL 复用同一条 Connection 的配置。
|
||
|
||
## 配置项
|
||
|
||
### 应用配置
|
||
|
||
| 环境变量 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `SPARK_EXECUTOR_DATA_DIR` | `./data` | 连接、pending job、job 记录和日志的基础目录 |
|
||
| `SPARK_EXECUTOR_JOBS_DIR` | `<SPARK_EXECUTOR_DATA_DIR>/jobs` | `write_job_file` 写入 PySpark 脚本的位置 |
|
||
| `SPARK_EXECUTOR_LOG_DIR` | `<SPARK_EXECUTOR_DATA_DIR>/logs` | loguru 文件日志目录 |
|
||
| `YARN_RESOURCE_MANAGER_URL` | 空 | 当 Connection 或 Job 快照中没有 `yarn_rm_url` 时的 fallback |
|
||
| `SPARK_EXECUTOR_LOG_LEVEL` | `DEBUG` | stderr 和 info 日志级别;debug 文件始终记录 DEBUG |
|
||
| `SPARK_EXECUTOR_SPARK_SUBMIT_BIN` | `spark-submit` | 调用 Spark 客户端的命令名或 wrapper 路径 |
|
||
| `SPARK_EXECUTOR_SSL_VERIFY_DEFAULT` | `true` | YARN REST 默认是否校验证书 |
|
||
| `SPARK_EXECUTOR_SSL_CA_BUNDLE_DEFAULT` | 空 | YARN REST 默认 CA bundle |
|
||
| `MCP_SERVICES` | 未设置 | 服务白名单;例如 `spark_executor,files`。未设置表示启用全部内置服务,空字符串表示不启用任何服务 |
|
||
| `FILES_MCP_ROOT` | Docker 中为 `/app/data/files` | Files MCP 能访问的唯一沙箱根目录 |
|
||
|
||
### Gunicorn 配置
|
||
|
||
| 环境变量 | 默认值 | 说明 |
|
||
| --- | --- | --- |
|
||
| `GUNICORN_BIND` | `0.0.0.0:8000` | 监听地址 |
|
||
| `GUNICORN_WORKERS` | `2` | worker 进程数 |
|
||
| `GUNICORN_THREADS` | `1` | 每个 worker 的线程数 |
|
||
| `GUNICORN_TIMEOUT` | `120` | 请求超时时间,YARN 日志可能较慢 |
|
||
| `GUNICORN_GRACEFUL_TIMEOUT` | `30` | 优雅退出超时 |
|
||
| `GUNICORN_KEEPALIVE` | `5` | keepalive 秒数 |
|
||
| `GUNICORN_LOGLEVEL` | `info` | gunicorn 日志级别 |
|
||
|
||
> **重要:生产上建议 `GUNICORN_WORKERS=1`,除非你在前面放了 sticky-session 反向代理。**
|
||
>
|
||
> MCP session 当前存储在 worker 进程内存里。多 worker 下,`initialize` 可能落到 worker A,下一次 `tools/list` 落到 worker B,然后 B 不认识这个 `mcp-session-id`,就会返回 `Session not found` 或 `Invalid or expired session ID`。这不是玄学,是进程内状态被 round-robin 打散了。
|
||
|
||
## 数据持久化
|
||
|
||
默认数据目录是 `./data/`,可通过 `SPARK_EXECUTOR_DATA_DIR` 覆盖。
|
||
|
||
| 路径 | 说明 |
|
||
| --- | --- |
|
||
| `data/connections.json` | 保存的 `Connection` 记录 |
|
||
| `data/pending_jobs.json` | `PendingSubmission` 记录 |
|
||
| `data/jobs.json` | 已提交 `Job` 记录 |
|
||
| `data/jobs.json.lock` | `jobs.json` 跨进程写入锁 |
|
||
| `data/jobs/` | 默认 PySpark 脚本写入目录 |
|
||
| `data/logs/debug/YYYY-MM-DD.log` | DEBUG 日志,按天轮转,保留 30 天,gzip 压缩 |
|
||
| `data/logs/info/YYYY-MM-DD.log` | INFO 日志,按天轮转,保留 30 天,gzip 压缩 |
|
||
| `data/files/` | Docker 默认 Files MCP 沙箱目录 |
|
||
|
||
`connections.json`、`pending_jobs.json` 和 `jobs.json` 都使用简单 JSON 文件持久化。当前这是务实方案:足够简单,方便排查,也方便未来迁移到 SQLite。
|
||
|
||
## Docker 部署
|
||
|
||
### 1. 准备 Hadoop/YARN 配置
|
||
|
||
在项目根目录创建或挂载:
|
||
|
||
```text
|
||
./hadoop-conf/
|
||
```
|
||
|
||
至少应包含目标集群匹配的:
|
||
|
||
```text
|
||
core-site.xml
|
||
yarn-site.xml
|
||
hdfs-site.xml
|
||
```
|
||
|
||
这些文件会以只读方式挂载到容器内:
|
||
|
||
```text
|
||
/etc/hadoop/conf
|
||
```
|
||
|
||
### 2. 准备环境变量
|
||
|
||
复制模板:
|
||
|
||
```bash
|
||
cp .env.example .env
|
||
```
|
||
|
||
按部署环境修改 `.env`,至少确认:
|
||
|
||
```text
|
||
YARN_RESOURCE_MANAGER_URL=
|
||
SPARK_EXECUTOR_DATA_DIR=
|
||
SPARK_EXECUTOR_JOBS_DIR=
|
||
FILES_MCP_ROOT=
|
||
GUNICORN_WORKERS=1
|
||
```
|
||
|
||
`GUNICORN_WORKERS=1` 不是装饰品。除非你真的配置了 sticky-session,否则不要把它调大。
|
||
|
||
### 3. 启动容器
|
||
|
||
```bash
|
||
docker compose up -d --build
|
||
```
|
||
|
||
查看日志:
|
||
|
||
```bash
|
||
docker compose logs -f mcp-server
|
||
```
|
||
|
||
停止服务:
|
||
|
||
```bash
|
||
docker compose down
|
||
```
|
||
|
||
## 测试
|
||
|
||
运行完整测试:
|
||
|
||
```bash
|
||
uv run pytest
|
||
```
|
||
|
||
运行单个测试文件:
|
||
|
||
```bash
|
||
uv run pytest tests/unit/test_submit_tool.py -v
|
||
```
|
||
|
||
按名称过滤:
|
||
|
||
```bash
|
||
uv run pytest -v -k cancel_pending
|
||
```
|
||
|
||
测试目录分为:
|
||
|
||
```text
|
||
tests/unit/ # 单元测试
|
||
tests/integration/ # FastAPI / MCP 集成测试
|
||
```
|
||
|
||
## 目录结构
|
||
|
||
```text
|
||
main.py # 根 FastAPI app;lifespan 中发现并挂载 MCP 服务
|
||
common/
|
||
config.py # 环境变量配置入口
|
||
factory.py # FastApiMCP 初始化封装
|
||
logging.py # loguru 配置
|
||
mcp_service.py # MCP 服务注册、发现、过滤
|
||
sql_guard.py # PySpark/SQL 安全策略
|
||
spark_executor/
|
||
server.py # Spark Executor FastAPI 子应用和 MCP tool routes
|
||
service.py # Spark Executor MCP 服务声明
|
||
models.py # Pydantic 数据模型
|
||
core/ # spark-submit、YARN client、持久化 store、日志解析
|
||
tools/ # MCP 工具业务函数和请求模型
|
||
files_mcp/
|
||
server.py # Files MCP FastAPI 子应用和 MCP tool routes
|
||
service.py # Files MCP 服务声明
|
||
core/ # 文件系统操作和路径沙箱校验
|
||
tools/ # 文件工具业务函数和请求模型
|
||
data/ # 默认运行时数据目录,gitignored
|
||
tests/ # 单元测试和集成测试
|
||
Dockerfile # 生产镜像
|
||
docker-compose.yml # 本地/生产容器启动模板
|
||
gunicorn.conf.py # gunicorn 运行配置
|
||
pyproject.toml # Python 项目和依赖声明
|
||
uv.lock # uv 锁文件
|
||
```
|
||
|
||
## 开发维护说明
|
||
|
||
### 新增一个 MCP 服务
|
||
|
||
新增服务的约定很简单,不要搞复杂框架:
|
||
|
||
1. 创建一个 Python package,例如 `my_service/`。
|
||
2. 在 package 内提供 FastAPI app。
|
||
3. 新增 `my_service/service.py`,导出 `SERVICE: McpService`。
|
||
4. 把 `"my_service.service"` 加到 `common/mcp_service.py` 的 `BUILTIN_SERVICES`。
|
||
5. 启动时根应用会自动导入、校验并挂载。
|
||
|
||
示例:
|
||
|
||
```python
|
||
from common.mcp_service import McpService
|
||
from my_service import app
|
||
|
||
SERVICE = McpService(
|
||
name="my_service",
|
||
app=app,
|
||
mount_path="/my-service-mcp",
|
||
)
|
||
```
|
||
|
||
### 新增 MCP 工具路由
|
||
|
||
所有 MCP 工具路由都应使用 **Pydantic body model**,不要用 query 参数传复杂对象。
|
||
|
||
正确形态:
|
||
|
||
```python
|
||
@app.post("/tool_name", operation_id="tool_name")
|
||
def _tool_name(req: ToolNameRequest):
|
||
return tool_name(**req.model_dump())
|
||
```
|
||
|
||
原因很简单:`fastapi-mcp` 的 `tools/call` 会把参数作为 JSON body 传给 FastAPI。复杂 dict 如果走 query 参数,会变成字符串,最后 422。
|
||
|
||
### 错误处理约定
|
||
|
||
工具函数内部保持干净:
|
||
|
||
- 找不到资源时抛 `KeyError`。
|
||
- 非法状态或非法参数抛 `ValueError`。
|
||
- 路由层由 FastAPI exception handler 转成 HTTP 状态码。
|
||
|
||
当前约定:
|
||
|
||
| 异常 | HTTP 状态 |
|
||
| --- | --- |
|
||
| `KeyError` | 404 |
|
||
| `ValueError` | 400 |
|
||
| Pydantic validation error | 422 |
|
||
| `FileExistsError`(Files MCP) | 409 |
|
||
|
||
不要在每个 route body 里手写一堆 `try/except`。那是噪音,不是设计。
|
||
|
||
### 文件头约定
|
||
|
||
新增 Python 文件时保持当前项目风格:
|
||
|
||
```python
|
||
# coding=utf-8
|
||
"""
|
||
@Time :YYYY/M/D
|
||
@Author :tao.chen
|
||
"""
|
||
```
|
||
|
||
### 日志约定
|
||
|
||
使用项目统一 logger:
|
||
|
||
```python
|
||
from common.logging import logger
|
||
```
|
||
|
||
不要在业务模块里自己创建一套 logger。
|
||
|
||
## 注意事项
|
||
|
||
1. **两步提交是核心设计,不是可选项。** `prepare_submit_job` 只创建 pending 记录,`confirm_submit_job` 才能执行 `spark-submit`。
|
||
2. **不要提交虚构脚本路径。** LLM 生成的 PySpark 必须先用 `write_job_file` 写入服务端文件系统。
|
||
3. **pending job 快照连接配置。** prepare 后再修改 Connection,不会影响已经创建的 pending job。
|
||
4. **MCP 请求需要 session affinity。** gunicorn 多 worker 会打散内存 session;默认部署请设 `GUNICORN_WORKERS=1`。
|
||
5. **`job_id` 参数兼容两种 ID。** 本地 `job_id` 和 YARN `application_id` 都可以传。
|
||
6. **Files MCP 只能访问沙箱目录。** 想让它访问某个目录,就把那个目录挂载并设置为 `FILES_MCP_ROOT`,不要绕路径校验。
|
||
7. **新工具必须使用 Pydantic body model。** 这是 fastapi-mcp 的调用形态决定的,不是个人偏好。
|
||
|
||
## 当前状态
|
||
|
||
- Spark Executor MCP:已实现连接管理、两步提交、作业状态/结果/日志/Kill、PySpark 文件写入/读取/更新。
|
||
- Files MCP:已实现沙箱文件 CRUD、搜索、移动、复制。
|
||
- 持久化:当前使用 JSON 文件,`jobs.json` 带文件锁;后续可迁移到 SQLite。
|
||
- 测试:包含单元测试和集成测试,修改后运行 `uv run pytest`。
|