GPU_GUARD_MONOREPO/docs/superpowers/specs/2026-05-15-mysql-question-answering-design.md
2026-05-20 21:39:12 +08:00

453 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 导出。