Skip to main content
Glama
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 依赖边界扫描。
- 前端统一信封拆包、工具编辑器与测试运行面板构建通过。