# MySQL 智能问数设计

> 日期：2026-05-15
> 状态：Draft v2
> Owner：与 lixin 共识
> 范围：MySQL 只读数据源配置、Agent Tool、跨表 JOIN、问数 Prompt Skill

## 0. v2 架构修订

v2 根据系统架构 review 收紧了安全边界：

- `mysql_query` MVP 只允许 `SELECT`，不再允许自由 `SHOW`、`DESCRIBE`、`EXPLAIN`。
- schema、索引、外键和少量样例通过受控工具读取，不通过自由 SQL 读取。
- 增加表级白名单、表级黑名单和列级脱敏配置。
- 增加 JOIN 成本保护：查询超时、最大 JOIN 表数、禁止无条件 JOIN、结果行数硬上限。
- 增加连接池生命周期设计。
- 增加查询审计表。
- 明确新增公共 `SecretCryptoService`，不复用当前 SkillSecretService 的明文写入行为。
- 区分后台安全投影和 Agent 工具投影，`mysql_list_sources` 不向 Agent 暴露 host、username、passwordEncrypted。
- 对显式 `LIMIT` 做硬上限校验，禁止用超大 LIMIT 绕过结果限制。
- MVP 对 `maskedColumns` 采取保守策略：自由 SQL 直接引用脱敏列时拒绝执行，避免 alias 绕过。

## 1. 背景与目标

Neta 当前已经有完整的 Agent Tool 治理、Skill Runtime 和 Agent 配置体系。智能问数能力需要同时满足两类需求：

- 平台能力：配置 MySQL 连接、加密保存密码、控制哪些 Agent 可用、执行只读 SQL、限制超时和返回行数。
- 问数流程：让 Agent 先理解 schema，必要时澄清业务口径，再生成 SQL，最后解释结果、口径和限制。

因此本功能采用“Tool 做执行能力，Skill 做问数方法论”的组合方案。MySQL 连接和 SQL 执行进入 NetaClaw Tool 系统；问数策略进入一个 prompt skill，供需要问数的 Agent 绑定。

## 2. 非目标

第一版不实现以下能力：

- 不支持 PostgreSQL、SQLite、SQL Server、Oracle 等其他数据库。
- 不执行 INSERT、UPDATE、DELETE、CREATE、ALTER、DROP、TRUNCATE、CALL、事务语句或存储过程。
- 不做可视化 BI 图表、不新增复杂 dashboard。
- 不做自然语言到 SQL 专用模型微调。
- 不自动优化复杂 SQL，只提供 schema、样例和执行反馈，让 Agent 在只读约束内生成 SQL。
- 不跨多个数据源做 JOIN；跨表 JOIN 仅限同一个 MySQL 数据源和当前连接可访问的库。
- 第一版不允许用户通过 `mysql_query` 自由执行 `SHOW`、`DESCRIBE`、`EXPLAIN`；这些元信息由 `mysql_schema` 等受控工具提供。

## 3. 用户体验

管理员在后台新增 MySQL 数据源，填写名称、连接地址、端口、数据库名、账号和密码。密码只保存加密值，不在接口回显明文。

管理员在 Agent 配置中启用 `mysql` toolset，并按需绑定 `data-analyst-mysql` skill。用户在对话中问“上个月各门店销售额是多少”“按客户和订单表统计复购率”这类问题时，Agent 会先读取 schema，必要时读取表样例，再执行只读 SQL。回答中需要包含：

- 结论。
- 使用的 SQL。
- 关联口径，尤其是跨表 JOIN 的关联字段。
- 结果限制，例如采样、LIMIT、字段缺失或关联字段为推断关系。

## 4. 架构

整体架构沿用 NetaClaw 当前工具链路：

```text
netaclaw_data_source + netaclaw_data_source_query_audit
  -> SecretCryptoService + DataSourceService 授权过滤
  -> MysqlPoolManager + MysqlQueryService / MysqlIntrospectionService
  -> mysql_list_sources / mysql_schema / mysql_table_sample / mysql_query
  -> Tool Catalog + Tool Governance + Tool Resolver
  -> Agent Runtime
  -> data-analyst-mysql Skill 约束问数流程
```

新增能力分为四层：

1. 数据源配置层：保存 MySQL 连接和 Agent 授权。
2. MySQL 执行层：封装连接池、schema 查询、样例读取和只读 SQL 执行。
3. Tool 层：向 Agent 暴露稳定工具接口，并接入 catalog、resolver、runtime policy。
4. Skill 层：定义问数流程、JOIN 推理规则和回答格式。

## 5. 数据模型

新增实体 `NetaClawDataSourceEntity`，表名 `netaclaw_data_source`。

字段：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `name` | varchar(100), unique | 数据源唯一名，工具调用使用 |
| `label` | varchar(200) | 展示名 |
| `type` | varchar(20) | 固定为 `mysql` |
| `host` | varchar(255) | MySQL host |
| `port` | int | 默认 3306 |
| `database` | varchar(128) | 默认库名 |
| `username` | varchar(255) | 连接用户名 |
| `passwordEncrypted` | text | AES-256-GCM 加密密码 |
| `readonly` | int | 1 表示只读，MVP 固定为 1 |
| `status` | int | 0 禁用，1 启用 |
| `allowedAgentIds` | json | 为空表示不授权任何 Agent；数组内 Agent 可用 |
| `extra` | json | SSL、connectTimeout、queryTimeout、maxRows、allowedTables、blockedTables、maskedColumns、maxJoinTables 等扩展 |

`extra` 权限字段：

| 字段 | 说明 |
| --- | --- |
| `allowedTables` | 可访问表白名单，空数组表示默认不开放任何业务表 |
| `blockedTables` | 表黑名单，优先级高于白名单 |
| `maskedColumns` | 需要脱敏的列，格式为 `{ "table.column": "hash|partial|redact" }` |
| `schemaVisibility` | `allowed-only` 或 `all-names-only`，MVP 默认 `allowed-only` |
| `queryTimeoutMs` | 单次 SQL 超时，默认 10000，硬上限 30000 |
| `maxRows` | 默认 200，硬上限 1000 |
| `maxJoinTables` | 默认 4，硬上限 6 |
| `poolConnectionLimit` | 默认 3，硬上限 10 |

密码必须用新增公共 `SecretCryptoService` 加密保存。当前 `SkillSecretService.encrypt()` 新写入路径是 JSON 明文，不能作为数据源密码的加密实现直接复用。

服务端必须区分两种投影：

- 管理端安全投影：可返回 `host`、`port`、`database`、`username`、`hasPassword`、授权与权限配置，但不返回 `passwordEncrypted` 或明文密码。
- Agent 工具投影：`mysql_list_sources` 只返回 `name`、`label`、`database`、`status`，不返回 `host`、`username`、`passwordEncrypted`、权限细节或连接串。

新增实体 `NetaClawDataSourceQueryAuditEntity`，表名 `netaclaw_data_source_query_audit`。

字段：

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `dataSourceId` | int | 数据源 ID |
| `agentId` | int | 调用 Agent |
| `userId` | int | 当前用户，可为空 |
| `toolCallId` | varchar(100) | 工具调用 ID |
| `sqlHash` | varchar(64) | SQL SHA-256 |
| `sqlPreview` | text | 截断后的 SQL 摘要，不含结果集 |
| `status` | varchar(20) | success / rejected / failed |
| `rejectReason` | varchar(100) | 被 guard 拒绝原因 |
| `elapsedMs` | int | 耗时 |
| `rowCount` | int | 返回行数 |
| `errorCode` | varchar(50) | MySQL errno / SQLSTATE，可为空 |

## 6. 后端服务

新增 `SecretCryptoService`：

- `encryptJson(value: Record<string, string>): string`
- `decryptJson(ciphertext: string): Record<string, string>`
- `encryptText(value: string): string`
- `decryptText(ciphertext: string): string`
- 使用 AES-256-GCM，密钥来自 `NETA_SECRET_KEY`，开发环境可回退到 `APP_SECRET`。
- 密文必须带版本化 envelope，例如 base64(JSON.stringify({ v: 1, alg: 'aes-256-gcm', iv, tag, ct }))，便于后续密钥轮换、格式识别和 Skill secret 迁移。
- 新数据源密码只允许写入加密格式；Skill secret 的现有兼容逻辑可后续迁移到该公共服务。

新增 `DataSourceService`：

- `listForAgent(agentId: number): Promise<AgentDataSourceSummary[]>`
- `listAdminSafe(): Promise<AdminSafeDataSource[]>`
- `getAuthorizedSource(name: string, agentId: number): Promise<NetaClawDataSourceEntity>`
- `saveConfig(input): Promise<AdminSafeDataSource>`
- `testConnection(input): Promise<{ ok: boolean; error?: string }>`
- 应用 `allowedAgentIds`、`allowedTables`、`blockedTables`、`maskedColumns` 和 `schemaVisibility`。
- 编辑已有数据源并测试连接时，如果没有传新密码，必须从数据库读取旧 `passwordEncrypted`，不能依赖接口回显密文。

新增 `MysqlPoolManager`：

- `getPool(source): Promise<Pool>`
- 按 `dataSource.id` 缓存连接池。
- 默认 `connectionLimit=3`，由 `extra.poolConnectionLimit` 覆盖但不得超过 10。
- 保存、禁用或删除数据源时关闭对应 pool。
- 连接失败时不泄漏 host、username、password，只返回脱敏错误。

新增 `MysqlIntrospectionService`：

- `listSchema(source, options): Promise<MysqlSchemaResult>`
- 读取 `information_schema.columns`、`statistics`、`key_column_usage`。
- 只返回授权表。字段输出要应用 `maskedColumns` 标记，让 Agent 知道该列敏感；MVP 中查询或样例请求直接引用该列时由后端拒绝，后续具备可靠列血缘解析后再支持脱敏展示。

新增 `MysqlQueryService`：

- `executeReadOnly(source, sql, options): Promise<MysqlQueryResult>`
- 使用 `mysql2/promise`。
- 每次查询设置超时、最大行数和结果截断。
- `SELECT` 查询若没有 `LIMIT`，外层包裹为 `SELECT * FROM (<sql>) AS neta_limited_query LIMIT ?`。
- 显式 `LIMIT` 必须小于等于 `maxRows`；超过上限直接拒绝或重写为安全上限，不能先拉取大结果再在 Node 中 `slice`。
- 执行前调用 SQL guard、授权表检查和 JOIN 成本检查。
- 自由 SQL 引用 `maskedColumns` 时，MVP 直接拒绝执行，避免 `SELECT email AS e` 绕过脱敏；后续只有在引入 SQL parser 并能追踪 source column 到 output alias 后，才允许查询脱敏列并在返回前脱敏。
- 执行错误必须通过 `sanitizeMysqlError(err)` 脱敏，只返回 `code`、`sqlState`、`errno` 和固定短文案，不返回原始 `err.message`。
- 执行后写 query audit，不记录结果集。

## 7. Tool 设计

新增 toolset：`mysql`。

### mysql_list_sources

输入：

```json
{}
```

输出：

```json
{
  "sources": [
    {
      "name": "sales_prod",
      "label": "销售生产库",
      "database": "sales",
      "status": 1
    }
  ]
}
```

只返回当前 Agent 授权的数据源摘要，不返回 host、username、passwordEncrypted、明文密码、连接串或表/列权限细节。

### mysql_schema

输入：

```json
{
  "source": "sales_prod",
  "tables": ["orders", "customers"]
}
```

`tables` 可选。为空时返回表摘要和字段摘要；指定时返回详细字段、索引和外键。

输出包含：

- `tables[].name`
- `tables[].comment`
- `tables[].columns[]`
- `tables[].primaryKey[]`
- `tables[].indexes[]`
- `tables[].foreignKeys[]`

### mysql_table_sample

输入：

```json
{
  "source": "sales_prod",
  "table": "orders",
  "columns": ["id", "customer_id", "created_at"],
  "limit": 5
}
```

用于辅助 Agent 判断字段含义和 JOIN key。`limit` 最大 20。表名和列名只能来自 introspection 结果，后端需要做 identifier 校验和反引号转义。

`mysql_table_sample` 不能通过拼接自由 SQL 绕过权限。后端必须先校验：

- `table` 在 `allowedTables` 内且不在 `blockedTables` 内。
- `columns` 均来自 `mysql_schema` 可见字段。
- 如果请求列命中 `maskedColumns`，MVP 返回拒绝错误；后续可加入专门脱敏样例能力。

### mysql_query

输入：

```json
{
  "source": "sales_prod",
  "sql": "SELECT c.name, COUNT(*) AS order_count FROM customers c JOIN orders o ON o.customer_id = c.id GROUP BY c.name ORDER BY order_count DESC LIMIT 20"
}
```

允许：

- `SELECT`
- 单数据源内的跨表 JOIN

禁止：

- 多语句。
- SQL 注释，包括 `--`、`#`、`/* ... */` 和 MySQL 版本注释。
- DML、DDL、DCL、事务和存储过程。
- `SELECT ... INTO OUTFILE`、`LOAD_FILE`、`SLEEP`、`BENCHMARK`。
- `SHOW`、`DESCRIBE`、`DESC`、`EXPLAIN` 自由 SQL。
- `UNION`、CTE、用户变量和临时表。
- 无限制返回大结果集。

输出：

```json
{
  "columns": ["name", "order_count"],
  "rows": [
    { "name": "A 客户", "order_count": 12 }
  ],
  "rowCount": 1,
  "truncated": false,
  "elapsedMs": 38,
  "sql": "SELECT ..."
}
```

## 8. SQL 安全策略

安全校验采用保守白名单规则。不要把 Skill prompt 当作安全边界，所有强制规则必须在 Tool/Service 层实现。

- 第一版优先采用 SQL parser 或词法 token guard；如果没有稳定 parser，就采用更严格的拒绝策略。
- 只允许单条 `SELECT`。
- 拒绝 SQL 注释，避免版本注释和注释夹带绕过。
- 拒绝包含分号的多语句。
- 拒绝非白名单开头语句。
- 拒绝 `UNION`、CTE、`INTO`、用户变量、临时表、危险函数和文件访问能力。
- 查询涉及的表必须全部落在 `allowedTables` 内，并且不在 `blockedTables` 内。
- SQL guard 必须逐个检查 `JOIN` 片段：每个显式 `JOIN` 在下一个 `JOIN`、`WHERE`、`GROUP BY`、`ORDER BY`、`LIMIT` 或语句结束前都必须出现 `ON` 或 `USING`。
- 自由 SQL 引用 `maskedColumns` 时直接拒绝，避免 alias 绕过。
- `SELECT` 必须限制行数；无 LIMIT 时后端包裹追加限制。
- 显式 LIMIT 必须不超过 `maxRows`。
- 查询超时默认 10 秒。
- 最大返回行数默认 200，硬上限 1000。
- 默认最大 JOIN 表数 4，硬上限 6。
- 拒绝无 `ON` / `USING` 的显式 JOIN，避免笛卡尔积。
- 错误信息只返回 SQLSTATE、errno/code 和固定短文案，不返回原始连接错误、连接密码、host、username 或完整连接串。
- 每次查询无论成功、失败或被拒绝，都写入审计记录。

这层只保证工具层安全，不负责判断业务 SQL 是否“正确”。业务口径由 Skill 和 Agent 交互处理。

## 9. 跨表 JOIN 策略

跨表 JOIN 是 MVP 能力。Agent 可生成多表 `JOIN`、`LEFT JOIN` 和聚合查询，但必须遵循 Skill 约束：

- 先调用 `mysql_schema` 获取相关表字段、索引和外键。
- 有外键时优先使用外键关系。
- 无外键时，可根据字段名、字段类型、索引和样例值推断关联字段。
- 推断关联必须在回答中标明，例如“orders.customer_id 与 customers.id 为字段名和样例推断关联”。
- 对不确定的关联关系或业务口径，必须先向用户澄清。
- JOIN 查询仍受 `mysql_query` 的只读、超时、LIMIT 和最大行数限制。
- JOIN 查询不得超过 `maxJoinTables`。
- `JOIN` 必须带 `ON` 或 `USING`，不允许隐式逗号连接。
- 每个 `JOIN` 都必须有自己的 `ON` 或 `USING` 条件，不能只检查整条 SQL 是否出现过一次 `ON`。
- 如果查询因超时或成本限制失败，Agent 应缩小时间范围、减少表数量、添加过滤条件或向用户澄清。

## 10. Skill 设计

新增 prompt skill：`packages/backend/skills/data-analyst-mysql/SKILL.md`。

Frontmatter：

```yaml
---
name: data-analyst-mysql
description: 使用 MySQL 工具进行只读智能问数，支持 schema 分析、跨表 JOIN、口径澄清和 SQL 结果解释。
version: 1.0.0
metadata:
  skillType: llm
  tags: [mysql, data-analysis, question-answering]
  conditions:
    requires_tools: ["mysql_list_sources", "mysql_schema", "mysql_query"]
---
```

Skill 指令要求：

- 先确认可用数据源。
- 先读取相关 schema，再写 SQL。
- 需要 JOIN 时先找外键、索引和样例值。
- 跨表 JOIN 不确定时先解释拟采用的关联字段，必要时澄清。
- 不确定业务口径时使用 `clarify`。
- 只执行只读 SQL。
- 回答必须包含结论、SQL、口径说明和限制。
- 不暴露连接地址、账号、密码。
- 不承诺“全量统计”除非 SQL 口径和过滤范围明确。

## 11. Tool Resolver 和 Runtime Policy

`tools/catalog.ts` 新增 `TOOLSET_MYSQL = 'mysql'`，并集中 import MySQL tool 文件。

MySQL 工具注册：

- `visibility: 'tool'`
- `capability: 'text'`
- `isCore: false`
- `canDisable: true`

主 Agent 可直接执行 MySQL 工具。子 Agent 默认不在 worker 本地执行 MySQL 工具，runtime policy 应将这类工具路由为 `main-process-proxy` 或 `disabled`。推荐第一版走 `main-process-proxy`，避免子进程持有数据库密钥。

## 12. 管理接口

新增 admin controller：`controller/admin/data_source.ts`。

接口：

- `GET /admin/netaclaw/data-source/list`
- `POST /admin/netaclaw/data-source/save`
- `POST /admin/netaclaw/data-source/test`
- `POST /admin/netaclaw/data-source/delete`

保存接口接受明文密码，但只用于加密保存。若编辑时密码为空，则保留原密码。

Agent 编辑页后续可接入授权配置，但 MVP 后端先支持 `allowedAgentIds` 字段和接口。

## 13. 测试策略

测试采用 TDD。

优先新增单元测试：

- `mysql_sql_guard.test.ts`：验证只读 SQL 白名单、危险关键字、多语句、`SELECT INTO OUTFILE`、`SLEEP` 拒绝。
- `mysql_sql_guard.test.ts` 还要验证 `SHOW`、`DESCRIBE`、`EXPLAIN`、注释、`UNION`、CTE、用户变量、无条件 JOIN、部分 JOIN 缺少 ON/USING、显式 LIMIT 超过 maxRows、查询脱敏列、超过最大 JOIN 表数被拒绝。
- `mysql_schema_mapper.test.ts`：验证 information_schema 行映射为表、字段、索引和外键结构。
- `mysql_query_service.test.ts`：用 mock mysql pool 验证 LIMIT 包裹、超时参数、结果截断和错误脱敏。
- `mysql_query_service.test.ts` 还要验证表级授权、脱敏列拒绝、审计写入、pool 复用和 `mysql_table_sample` identifier 白名单。
- `secret_crypto.test.ts`：验证数据源密码使用 AES-GCM 加密，密文不包含明文密码。
- `data_source_projection.test.ts`：验证管理端投影不返回密码，Agent 投影不返回 host/username/passwordEncrypted。
- `tool_resolver.test.ts`：验证 `mysql` toolset 可以被 Agent 启用，并在子 Agent runtime profile 中不走 worker-local。
- `entity_exports.test.ts`：验证新增 entity 导出。

不在单元测试里连接真实 MySQL。真实连接测试通过 admin `test` 接口和后续手工环境验证完成。

## 14. 分阶段交付

### Phase 1：后端最小闭环

- 新增数据源 entity、公共 secret crypto service、data source service。
- 新增 query audit entity。
- 新增公共 `SecretCryptoService`。
- 新增 `MysqlPoolManager`。
- 新增 MySQL SQL guard、schema mapper、query service。
- 新增四个工具：`mysql_list_sources`、`mysql_schema`、`mysql_table_sample`、`mysql_query`。
- 接入 catalog、resolver、runtime policy。
- 新增 `data-analyst-mysql` prompt skill。
- 补充单元测试。

### Phase 2：管理端配置

- 新增数据源管理接口。
- 前端工具管理或 Agent 编辑页接入数据源授权。
- 支持测试连接和编辑密码。

### Phase 3：问数体验增强

- 根据真实问数记录优化 Skill。
- 增加 query result 前端结构化渲染。
- 增加审计日志查询页和慢查询提示。

## 15. 验收标准

- 管理员可以配置一个 MySQL 数据源并授权给指定 Agent。
- 数据源密码以 AES-GCM 密文保存，接口不回显密文或明文。
- 启用 `mysql` toolset 的 Agent 能列出授权数据源。
- Agent 只能读取授权表的 schema、字段、索引和外键。
- Agent 能读取授权表的少量样例。
- Agent 能执行只读 SELECT 查询。
- Agent 能执行同一数据源、授权表范围内的跨表 JOIN 查询。
- `SHOW`、`DESCRIBE`、`EXPLAIN`、多语句、注释、写入语句、无条件 JOIN 和超出 JOIN 表数限制的 SQL 被拒绝。
- 查询或样例请求命中 `maskedColumns` 时，MVP 后端拒绝执行，避免 alias 绕过；后续如引入 SQL parser 再实现安全脱敏返回。
- 每次查询写入审计记录。
- 危险 SQL 被拒绝，错误原因可读且不泄漏连接秘密。
- Agent 绑定 `data-analyst-mysql` skill 后，回答包含结论、SQL、口径和限制。
- 单元测试覆盖 SQL guard、schema mapper、query service、secret crypto、pool manager、tool resolver 和 entity 导出。
