dd-arch
by ollo-kitty
README.md
# dd-arch
DDD 架构 · MCP 工具管理平台。
## 快速启动
启动 MySQL 8.0(首次创建数据卷时会执行 [docs/init.sql](docs/init.sql)):
```bash
docker compose -f docs/docker-compose.yml up -d
```
安装后端依赖:
```bash
uv sync --extra dev
```
安装并构建前端:
```bash
cd frontend
npm install
npm run build
cd ..
```
启动 HTTP + MCP Streamable HTTP:
```bash
source .venv/bin/activate
python -m dd_arch.app.application
```
如果没有激活虚拟环境,也可以直接通过 `uv` 调用:
```bash
uv run python -m dd_arch.app.application
```
启动后主要入口:
```text
/api 管理端 REST API
/mcp MCP Streamable HTTP
/ frontend/dist 静态前端
```
停止 MySQL:
```bash
docker compose -f docs/docker-compose.yml down
```
如需重新执行初始化 SQL,先删除数据卷:
```bash
docker compose -f docs/docker-compose.yml down -v
docker compose -f docs/docker-compose.yml up -d
```
> ⚠️ **安全前提(RCE)**:本平台允许从 Web 提交并在服务器进程内执行任意 Python 代码,本质是远程代码执行。当前采用**进程内 `exec`(最简,完全 RCE,无沙箱)**,**仅适用于受信任的内网/单用户环境**。编辑类接口必须置于受信任网络或加鉴权后再对外。命名空间预置的最小 `__builtins__` 不构成安全边界;沙箱隔离是预留扩展点(`IToolExecutorPort` 可替换为受限/子进程实现)。
权威规格见 [spec.md](docs/spec.md),执行进度见 [progress.md](docs/progress.md)。
## 架构总览
项目采用轻量六边形架构,并额外保留通用类型层和启动层。工具来自数据库,不再有静态注册中心或工具目录。
```text
+-------------+
| trigger | HTTP / MCP 入站适配
+------+------+
|
+------v------+
| api | DTO / 响应契约
+------+------+
|
+------v------+
| domain | 领域模型 / 领域服务 / 接口
+------^------+
|
+---------------+---------------+
| infrastructure | DB / Cache / Event / Metrics / Executor
+-------------------------------+
types 提供跨层共享的错误码、异常、响应和分页类型。
app 作为组合根装配所有层,并负责生命周期。
```
核心约束:
- `domain` 不依赖 FastAPI、SQLAlchemy、MCP、infrastructure 等外部技术;也不真正 `exec` 代码(编译执行是 `IToolExecutorPort` 的 infra 职责,domain 只定义接口)。
- 简单编排直接放在 `domain service` 中;`trigger` 只做协议适配、参数接收、响应包装。
- `infrastructure` 实现领域层定义的 repository/port 接口,不反向依赖 `trigger`。
- `app` 是唯一知道所有层实现细节的组合根,负责依赖注入、生命周期和挂载。
- 数据库、repository、service 主链路使用同步调用;FastAPI/MCP 入口保留 async 协议函数,并通过线程池调用同步服务。
## 项目结构
```text
.
├── resources/
│ └── application.yaml # 默认运行配置
├── docs/init.sql # MySQL schema
├── pyproject.toml # 项目依赖和构建配置
├── docs/spec.md # 项目权威规格
├── docs/progress.md # 执行进度
├── docs/step-*.md # 分步骤规格
├── tests/ # 自动化测试
├── frontend/ # Vue 管理端(含工具编辑器)
└── src/dd_arch/
├── types/ # 通用基础类型
├── api/ # HTTP/API 契约
├── domain/ # 领域核心(tooldef / invocation)
├── infrastructure/ # 外部技术实现(含 ToolExecutorPort)
├── trigger/ # HTTP/MCP 入站适配器
└── app/ # 组合根、启动入口和运行配置加载
```
## 分层职责
### `types/` 通用类型层
这一层放跨层共享且不携带业务流程的基础类型,主要包括:
- 统一错误码。
- 工具异常和业务异常。
- HTTP 统一响应信封 `{code, info, data}`。
- 内部分页结果。
它可以被各层引用,但不主动依赖业务层或基础设施层。
### `api/` 契约层
这一层描述管理端 API 的输入输出契约,主要包括:
- 工具定义、服务状态、调用日志、日志统计等 DTO。
- API 错误响应构造工具。
`api` 层不处理业务流程,只定义 trigger 和 service 之间稳定的数据形状。
### `domain/` 领域层
领域层是业务核心,不包含框架代码和数据库代码。
```text
domain/
├── tooldef/ # 工具定义子域(Web 动态工具核心)
│ ├── model/ # ToolDefinitionAggregate / 参数与状态值对象 / 编译产物 VO
│ ├── adapter/repository/ # 工具定义、工具配置仓储接口
│ ├── adapter/port/ # 代码执行器端口接口(IToolExecutorPort)
│ └── service/ # 参数→JSON Schema、参数矫正等领域规则
└── invocation/ # 工具调用子域
├── model/ # 调用日志、调用事件、状态值对象
├── adapter/repository/ # 调用日志仓储接口
├── adapter/port/ # 事件发布、指标等端口接口
└── service/ # 调用日志和统计领域规则
```
这一层解决的问题:
- 一个动态工具如何表达:名称、描述、代码串、是否异步、tags、参数列表、状态(DRAFT/ENABLED/DISABLED)。
- 参数列表如何生成 JSON Schema,原始入参如何按类型矫正。
- 每个工具的运行时配置如何表达。
- 工具调用成功、失败、耗时、错误如何记录为领域事实。
- 调用事件如何作为领域事件发布给外部处理器。
- repository/port 的能力边界是什么(Repository=本地数据,Port=代码执行/事件/指标等外部能力)。
### `infrastructure/` 基础设施层
基础设施层负责把领域接口落到具体技术实现。
```text
infrastructure/
├── dao/ # SQLAlchemy 查询封装
│ └── po/ # ORM 持久化对象(tool_definition / tool_invocation_log)
├── adapter/
│ ├── repository/ # 领域 repository 的实现
│ └── port/ # 领域 port 的实现(含 ToolExecutorPort)
├── cache/ # 进程内 TTL 缓存
└── logging/ # 审计日志后台批量写入
```
这一层解决的问题:
- 数据库表和领域对象之间的转换(tags/parameters/status 的 JSON↔对象)。
- 工具定义、调用日志的持久化。
- 工具定义的 cache-aside 缓存。
- **代码执行器**(`ToolExecutorPort`):把工具的 `code` 串 `compile`+`exec` 到受控命名空间取出入口函数,按 `name+sha256(code)` 缓存编译产物,工具更新即 `invalidate`,按 `is_async` 分派。
- 领域事件的进程内分发。
- 审计日志后台批量写入。
- 配置文件和数据库连接。
领域服务同时负责这些简单编排:
- 工具创建/更新时构造聚合、校验、编译验证(编译失败回滚),并失效执行器缓存。
- 工具执行前检查工具是否存在、是否启用、是否需要 `ctx` 配置。
- 工具执行支持普通 `def handler(...)` 和 `async def handler(...)`。
- 将执行成功或失败转为领域事件。
- 草稿试跑(用编辑中、尚未保存的临时定义即时编译执行)与已存工具试跑(绕过 ENABLED 校验),两者记 `source='test'` 日志、不发 MCP 调用事件。
- 分页查询日志、查询详情、统计和清理历史日志。
- 将领域事件转为审计日志入队。
### `trigger/` 触发层
触发层是入站适配器,负责把外部协议转换为用例调用。
```text
trigger/
├── http/ # FastAPI 管理 API
└── mcp/ # 官方 MCP low-level server 和 Streamable HTTP
```
HTTP 侧主要提供:
```text
GET /api/status
GET /api/tools
GET /api/tools/{name}
POST /api/tools # 创建:code + parameters + is_async + tags + config
PUT /api/tools/{name} # 更新:同创建请求体
DELETE /api/tools/{name} # 删除
POST /api/tools/{name}/enable
POST /api/tools/{name}/disable
POST /api/tools/test # 草稿试跑(body 含 definition,不读/写库)
POST /api/tools/{name}/test # 已存工具试跑(绕过 ENABLED)
GET /api/logs
GET /api/logs/stats
GET /api/logs/{id}
DELETE /api/logs?before=ISO_DATETIME
```
MCP 侧主要提供:
- `tools/list`:只暴露启用状态的工具。
- `tools/call`:把 MCP 调用转换为工具执行用例。
- Streamable HTTP ASGI app:挂载到 FastAPI 的 `/mcp`。
### `app/` 启动层
启动层是组合根,负责把所有层连接起来。
```text
app/
├── application.py # 进程启动入口,按配置选择 HTTP 或 stdio
├── container.py # dependency-injector 容器和 ApplicationContext
├── persistence.py # session/事务边界和仓储装配
└── config/ # YAML 配置加载、数据库/FastAPI/MCP 组件配置
```
这一层解决的问题:
- 加载 `resources/application.yaml` 和可选 `resources/application-local.yaml`。
- 创建数据库 engine、session factory、DAO、repository、port(含 `ToolExecutorPort`)和 service。
- 工具全部来自 DB(`tool_definition` 表),由 Web 端创建,不做工具目录自动发现。
- 将事件订阅者注册到事件总线。
- 构建 HTTP routers、MCP server 和前端静态挂载。
- 在 lifespan 中建表、启动/停止日志写入器、管理 MCP session manager、释放数据库连接。
## 关键运行链路
### 管理 API 链路
```text
HTTP request
-> trigger/http controller
-> domain service / repository interface
-> infrastructure repository / dao / cache
-> Response{code, info, data}
```
典型场景:
- 工具列表:返回工具定义(含代码、参数、状态、input_schema、运行时配置)。
- 创建/更新:在 service 中开启事务,校验并编译验证代码(编译失败回滚,返回 `COMPILE_ERROR`),写入后失效执行器缓存。
- 启用/禁用:写入状态并失效缓存。
- 配置随工具创建/更新写入,工具执行时若声明 `ctx` 可读取。
- 日志查询:读取调用日志表并转换为前端 DTO。
### MCP 工具调用链路
```text
MCP tools/call
-> trigger/mcp
-> 工具执行服务
-> 工具定义仓储查询(经缓存)
-> domain 检查工具状态
-> ToolExecutorPort 取/编译可调用对象(按 name+code 哈希缓存)
-> 按 is_async 分派执行(同步服务中封装 async handler)
-> 发布成功或失败领域事件
-> 审计日志订阅者入队
-> 文本结果返回 MCP client
```
这条链路保证:
- 未注册工具返回统一错误码(`UNKNOWN_TOOL`)。
- 禁用/草稿工具不会进入实际函数执行(`TOOL_DISABLED`)。
- HTTP/MCP 入口通过线程池调用同步用例,避免同步数据库操作直接阻塞事件循环。
- async 工具仍可执行,但会在线程内独立运行事件循环。
- 日志和指标由事件订阅者处理,不阻塞主调用流程。
## 数据模型
默认 MySQL schema 见 [init.sql](docs/init.sql):
```text
tool_definition
动态工具定义:code、is_async、tags_json、parameters_json、config_json、status
tool_invocation_log
工具调用日志、状态、耗时、错误、source(mcp/test)和创建时间
```
开发和测试可使用 `sqlite:///path/to/dev.db`。生产默认配置使用 `mysql+pymysql`。
## 配置
默认配置在 [resources/application.yaml](src/dd_arch/app/resources/application.yaml):
```yaml
server:
host: 127.0.0.1
port: 8003
mcp:
transport: streamable-http
http-path: /mcp
frontend-dist: frontend/dist
database:
url: mysql+pymysql://root:pwd@127.0.0.1:3306/mcp_admin?charset=utf8mb4
pool-size: 20
max-overflow: 10
```
本地敏感配置写入 `resources/application-local.yaml`,会覆盖默认配置,且已被 `.gitignore` 忽略。
## 运行说明
本地敏感配置可写入 `resources/application-local.yaml`。例如开发时可使用 SQLite:
```yaml
database:
url: sqlite:///./dev.db
```
直接用模块方式启动服务,host、port、MCP transport 和路径都从 `resources/application.yaml` 读取:
```bash
python -m dd_arch
```
开发前端时也可以单独启动 Vite:
```bash
cd frontend
npm run dev
```
Vite 会把 `/api` 和 `/mcp` 代理到 `http://127.0.0.1:8003`。
## 在 Web 端创建工具
1. 打开前端,进入 Tools 页,点击「New Tool」。
2. 填写:
- 名称(合法 Python 标识符)、描述。
- 代码框:定义入口函数 `def handler(...)` 或 `async def handler(...)`(异步工具需勾选「Async」开关并与代码一致)。需要读取运行时配置时,在签名里加 `ctx` 参数(`ctx.config` 为该工具的 KV 配置)。
- 参数列表:逐行配置参数名/类型/必填/默认/描述,保存时由后端生成 JSON Schema。
- tags(逗号分隔)。
3. **保存前**可在「Test Run」面板填入测试参数一键试跑(草稿试跑,无需先保存,编译失败会直接显示 `COMPILE_ERROR`)。
4. 点击「Save Tool」:后端编译校验通过后入库(默认 DRAFT),编译失败返回 `info` 并不落库。
5. 回到列表 enable 工具;启用后 MCP 客户端 `tools/list` 即可见、`tools/call` 可调用。
示例代码(同步):
```python
def handler(a, b):
return a + b
```
示例代码(异步):
```python
import asyncio
async def handler(seconds):
await asyncio.sleep(seconds)
return f"slept {seconds}s"
```
> 当前数据库主链路是同步的;HTTP/MCP 入口会把用例调用放入线程池执行。
## 测试
运行后端测试:
```bash
uv run pytest
```
运行前端类型检查和生产构建:
```bash
cd frontend
npm run build
```
当前测试覆盖:
- domain 不依赖 infrastructure、trigger、app、FastAPI、SQLAlchemy、MCP。
- executor:sync/async 工具编译与分派、语法错误与 `is_async` 不符返回 `COMPILE_ERROR`、`build_input_schema` 与 `coerce_arguments`(字符串转型与必填校验)。
- 工具 CRUD:创建→编译校验→enable→MCP 列表可见;坏代码 create 返回 `COMPILE_ERROR` 且不落库。
- 测试运行:草稿试跑返回结果且不创建工具、坏代码草稿试跑返回 `ok=false, code=COMPILE_ERROR`;已存工具试跑对 DRAFT/DISABLED 工具仍能执行;测试调用记 `source='test'` 且不计入 `stats()`。
- 执行用例:DISABLED 拦截并发 `ToolFailedEvent`;成功调用返回结果并经订阅者批量落库。
- 50 个 `async def handler` + `await asyncio.sleep(1)` 通过线程池并发调用总耗时小于 5 秒。
## 已验证内容
当前已完成的本地验证包括:
- `src/dd_arch` 导入检查(全包可 import,无残留旧模块引用)。
- `uv run pytest`(18 项全绿)。
- `frontend/npm run build`(vue-tsc 类型检查 + vite 构建通过)。
- SQLite 库下 FastAPI lifespan 启动和关闭、MCP session manager 初始化。
- `/api/status`、`/api/tools` CRUD、`/api/tools/test` 草稿试跑和 enable(经 ASGI 实测)。
- 工具定义 cache-aside、执行器编译缓存与失效。
- domain 依赖边界扫描。
- 前端统一信封拆包、工具编辑器与测试运行面板构建通过。