Compare commits
5 Commits
fb61ae27cf
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 635313a7ab | |||
| ba5cd4bbe1 | |||
| 557361632c | |||
| 9c597c9b0d | |||
| a1012e61bf |
451
.kilo/plans/lzwcai-agile-db-skill.md
Normal file
451
.kilo/plans/lzwcai-agile-db-skill.md
Normal file
@@ -0,0 +1,451 @@
|
|||||||
|
# lzwcai-agile-db Skill 实现计划
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
创建一个 Kilo skill 文件,为 AI Agent 提供 AgileDB 数据库操作的场景化工作流指导。
|
||||||
|
|
||||||
|
## Skill 文件路径
|
||||||
|
`.kilo/skills/lzwcai-agile-db/SKILL.md`
|
||||||
|
|
||||||
|
## Skill 内容
|
||||||
|
|
||||||
|
以下为完整的 SKILL.md 内容:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# lzwcai-agile-db
|
||||||
|
|
||||||
|
AgileDB 数据库操作技能。为 AI Agent 提供数据源浏览、表数据 CRUD、SQL 执行和 AI 生成表结构的场景化工作流。
|
||||||
|
|
||||||
|
## 可用 MCP 工具
|
||||||
|
|
||||||
|
本 skill 基于 `lzwcai_mcp_agile_db` MCP Server,提供以下工具:
|
||||||
|
|
||||||
|
### 数据源管理
|
||||||
|
| 工具 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `list_datasources` | 获取数据源列表 |
|
||||||
|
| `get_datasource_detail` | 获取数据源详情(含数据库、表结构) |
|
||||||
|
| `create_datasource` | 创建外部数据源 |
|
||||||
|
| `update_datasource` | 更新数据源 |
|
||||||
|
| `toggle_datasource_status` | 启用/停用数据源 |
|
||||||
|
| `delete_datasource` | 删除数据源 |
|
||||||
|
|
||||||
|
### 数据库与表管理
|
||||||
|
| 工具 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `list_databases` | 获取数据源下的数据库列表 |
|
||||||
|
| `list_tables` | 获取数据库下的表列表 |
|
||||||
|
| `get_table_detail` | 获取表结构详情(字段、类型、主键) |
|
||||||
|
| `create_table` | 创建新表 |
|
||||||
|
| `alter_table` | 修改表结构 |
|
||||||
|
| `generate_table_by_description` | AI 根据自然语言生成表结构 |
|
||||||
|
|
||||||
|
### 表数据操作
|
||||||
|
| 工具 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `query_table_data` | 查询表数据(分页) |
|
||||||
|
| `insert_table_row` | 插入一行数据 |
|
||||||
|
| `update_table_row` | 更新一行数据 |
|
||||||
|
| `delete_table_rows` | 删除数据行(按主键) |
|
||||||
|
| `export_table_excel` | 导出表数据为 Excel |
|
||||||
|
|
||||||
|
### SQL 执行
|
||||||
|
| 工具 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `execute_sql` | 执行原生 SQL 查询 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 场景 1:浏览数据源
|
||||||
|
|
||||||
|
当用户想要了解有哪些数据源、数据库或表时,使用此流程。
|
||||||
|
|
||||||
|
### 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求: "有哪些数据源?" / "看看 XX 数据源有哪些表?"
|
||||||
|
↓
|
||||||
|
1. 调用 list_datasources()
|
||||||
|
↓
|
||||||
|
2. 展示数据源列表(名称、类型、状态、数据库数、表数)
|
||||||
|
↓
|
||||||
|
3. 用户选择数据源后,调用 get_datasource_detail(datasourceId="xx")
|
||||||
|
↓
|
||||||
|
4. 展示数据库列表和实时表结构
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
**用户**: "帮我看看有哪些数据源"
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: list_datasources()
|
||||||
|
返回: [{id: "1", name: "订单系统", type: "builtin", status: 0, databases: 3, tables: 12}, ...]
|
||||||
|
回复: 共找到 3 个数据源:
|
||||||
|
- 订单系统(内置,运行中,3 个库,12 张表)
|
||||||
|
- 用户中心(外部 MySQL,运行中,1 个库,5 张表)
|
||||||
|
- 报表系统(外部 PostgreSQL,已停止)
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户**: "看看订单系统有哪些表"
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: get_datasource_detail(datasourceId="1")
|
||||||
|
返回: {datasource: {...}, databases: [{name: "order_db", tables: [...]}]}
|
||||||
|
回复: 订单系统包含以下数据库:
|
||||||
|
order_db:
|
||||||
|
- orders (订单表, 15 个字段)
|
||||||
|
- order_items (订单明细, 10 个字段)
|
||||||
|
- users (用户表, 8 个字段)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- 数据源状态:`0`=运行中,`1`=已停止
|
||||||
|
- 数据源类型:`builtin`=内置,`external`=外部
|
||||||
|
- 如果用户只说"查一下数据库",先调用 `list_datasources()` 再引导选择
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 场景 2:查询表数据
|
||||||
|
|
||||||
|
当用户想要查看某张表的具体数据记录时,使用此流程。
|
||||||
|
|
||||||
|
### 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求: "查一下 users 表的数据" / "看看前 20 条订单"
|
||||||
|
↓
|
||||||
|
1. 如果不知道 tableId,先浏览数据源找到目标表
|
||||||
|
↓
|
||||||
|
2. 调用 query_table_data(tableId="xx", pageNum=1, pageSize=10, target="prod")
|
||||||
|
↓
|
||||||
|
3. 展示数据(表格形式,包含字段名和值)
|
||||||
|
↓
|
||||||
|
4. 如果数据量大,提示用户可翻页或调整 pageSize
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
**用户**: "查一下 users 表前 10 条数据"
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: query_table_data(tableId="5", pageNum=1, pageSize=10)
|
||||||
|
返回: {
|
||||||
|
columns: [{name: "id", type: "INTEGER"}, {name: "username", type: "VARCHAR"}, ...],
|
||||||
|
data: [{id: 1, username: "admin", email: "admin@test.com"}, ...],
|
||||||
|
total: 156
|
||||||
|
}
|
||||||
|
回复: users 表共 156 条记录,当前显示第 1-10 条:
|
||||||
|
| id | username | email | created_at |
|
||||||
|
|----|----------|-----------------|---------------------|
|
||||||
|
| 1 | admin | admin@test.com | 2024-01-15 10:30:00 |
|
||||||
|
| 2 | user1 | user1@test.com | 2024-01-16 14:20:00 |
|
||||||
|
...
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- `target` 参数:`prod`=生产环境,`test`=测试环境,默认 `prod`
|
||||||
|
- 如果用户未指定数量,默认 `pageSize=10`
|
||||||
|
- 数据返回格式为对象数组(已自动转换表头)
|
||||||
|
- 如果用户说"翻页",增加 `pageNum` 参数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 场景 3:执行 SQL
|
||||||
|
|
||||||
|
当用户需要执行自定义 SQL 查询时,使用此流程。
|
||||||
|
|
||||||
|
### 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求: "帮我执行 SQL..." / "查一下订单数大于 100 的用户"
|
||||||
|
↓
|
||||||
|
1. 如果用户直接提供 SQL,直接使用
|
||||||
|
如果用户提供自然语言需求,先转换为 SQL
|
||||||
|
↓
|
||||||
|
2. 调用 execute_sql(datasourceId="xx", executableSql="SELECT ...")
|
||||||
|
↓
|
||||||
|
3. 展示查询结果
|
||||||
|
↓
|
||||||
|
4. 如果 SQL 执行失败,展示错误信息并建议修正
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
**用户**: "统计每个地区的订单数量"
|
||||||
|
|
||||||
|
```
|
||||||
|
转换为 SQL: SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC
|
||||||
|
|
||||||
|
调用: execute_sql(
|
||||||
|
datasourceId="1",
|
||||||
|
executableSql="SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC"
|
||||||
|
)
|
||||||
|
返回: {
|
||||||
|
columns: [{name: "region"}, {name: "order_count"}],
|
||||||
|
data: [{region: "华东", order_count: 1250}, {region: "华南", order_count: 980}, ...]
|
||||||
|
}
|
||||||
|
回复: 各地区订单统计:
|
||||||
|
| 地区 | 订单数 |
|
||||||
|
|------|--------|
|
||||||
|
| 华东 | 1,250 |
|
||||||
|
| 华南 | 980 |
|
||||||
|
| 华北 | 756 |
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- `datasourceId` 必须提供,如果用户未指定,先询问或使用最近使用的数据源
|
||||||
|
- `sqlTemplate` 可选,用于模板化查询
|
||||||
|
- `parameters` 可选,用于参数化查询(防止 SQL 注入)
|
||||||
|
- 执行危险操作(DELETE/DROP/UPDATE)前,**必须**向用户确认
|
||||||
|
- 如果查询结果超过 100 行,建议用户使用 `query_table_data` 代替
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 场景 4:增删改数据
|
||||||
|
|
||||||
|
当用户需要修改表中的数据时,使用此流程。
|
||||||
|
|
||||||
|
### 4.1 插入数据
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求: "新增一个用户,用户名是 test_user"
|
||||||
|
↓
|
||||||
|
1. 先调用 get_table_detail(tableId="xx") 了解表结构
|
||||||
|
↓
|
||||||
|
2. 确认必填字段(非空字段、无默认值字段)
|
||||||
|
↓
|
||||||
|
3. 调用 insert_table_row(tableId="xx", data={...})
|
||||||
|
↓
|
||||||
|
4. 确认插入成功
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
**用户**: "新增一个用户,用户名 test_user,邮箱 test@test.com"
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: get_table_detail(tableId="5")
|
||||||
|
返回: {columns: [{name: "id", isPrimaryKey: true, isAutoIncrement: true}, {name: "username", isNullable: false}, ...]}
|
||||||
|
|
||||||
|
调用: insert_table_row(
|
||||||
|
tableId="5",
|
||||||
|
data={"username": "test_user", "email": "test@test.com"}
|
||||||
|
)
|
||||||
|
回复: 已成功插入用户 test_user (ID 自动生成)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.2 更新数据
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求: "把用户 test_user 的邮箱改成 new@test.com"
|
||||||
|
↓
|
||||||
|
1. 确认主键字段和要更新的值
|
||||||
|
↓
|
||||||
|
2. 调用 update_table_row(tableId="xx", primaryKey={主键}, data={要更新的字段})
|
||||||
|
↓
|
||||||
|
3. 确认更新成功
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
**用户**: "把 ID 为 5 的用户邮箱改成 new@test.com"
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: update_table_row(
|
||||||
|
tableId="5",
|
||||||
|
primaryKey={"id": 5},
|
||||||
|
data={"email": "new@test.com"}
|
||||||
|
)
|
||||||
|
回复: 已更新用户 ID=5 的邮箱为 new@test.com
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.3 删除数据
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求: "删除用户 test_user"
|
||||||
|
↓
|
||||||
|
1. ⚠️ 危险操作!先确认删除范围和影响
|
||||||
|
↓
|
||||||
|
2. 向用户展示将要删除的数据,请求确认
|
||||||
|
↓
|
||||||
|
3. 用户确认后,调用 delete_table_rows(tableId="xx", primaryKeys=[{主键}])
|
||||||
|
↓
|
||||||
|
4. 确认删除成功
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
**用户**: "删除 ID 为 10 的订单"
|
||||||
|
|
||||||
|
```
|
||||||
|
回复: ⚠️ 确认要删除以下记录吗?
|
||||||
|
订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00
|
||||||
|
此操作不可恢复。请回复"确认删除"继续。
|
||||||
|
|
||||||
|
用户: "确认删除"
|
||||||
|
|
||||||
|
调用: delete_table_rows(
|
||||||
|
tableId="3",
|
||||||
|
primaryKeys=[{"id": 10}]
|
||||||
|
)
|
||||||
|
回复: 已成功删除订单 ID=10
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- **删除操作必须二次确认**
|
||||||
|
- `primaryKey` 必须是对象格式,如 `{"id": 1}`
|
||||||
|
- `data` 只包含要更新的字段,不需要提供全部字段
|
||||||
|
- 插入数据时,自增主键不需要提供
|
||||||
|
- 如果操作涉及多行,使用批量操作或循环调用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 场景 5:AI 生成表结构
|
||||||
|
|
||||||
|
当用户需要创建新表但不知道如何设计表结构时,使用 AI 辅助生成。
|
||||||
|
|
||||||
|
### 工作流程
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求: "我需要一个用户表" / "帮我设计一个订单系统的表结构"
|
||||||
|
↓
|
||||||
|
1. 调用 generate_table_by_description(requirement="用户描述")
|
||||||
|
↓
|
||||||
|
2. 展示 AI 生成的表结构(表名、字段、类型、注释)
|
||||||
|
↓
|
||||||
|
3. 用户确认后,调用 create_table(connectionId="xx", databaseName="xx", tableName="xx", columns=[...])
|
||||||
|
↓
|
||||||
|
4. 确认创建成功
|
||||||
|
```
|
||||||
|
|
||||||
|
### 示例
|
||||||
|
|
||||||
|
**用户**: "帮我设计一个商品表,包含名称、价格、库存、分类"
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: generate_table_by_description(
|
||||||
|
requirement="我需要一个商品表,包含商品名称、价格、库存、分类,用于电商系统"
|
||||||
|
)
|
||||||
|
返回: {
|
||||||
|
tableName: "products",
|
||||||
|
tableComment: "商品表",
|
||||||
|
columns: [
|
||||||
|
{columnName: "id", columnType: "SERIAL", isPrimaryKey: true, isAutoIncrement: true, columnComment: "主键ID"},
|
||||||
|
{columnName: "name", columnType: "VARCHAR", columnLength: 200, isNullable: false, columnComment: "商品名称"},
|
||||||
|
{columnName: "price", columnType: "DECIMAL", columnLength: 10, columnComment: "价格"},
|
||||||
|
{columnName: "stock", columnType: "INTEGER", columnComment: "库存数量"},
|
||||||
|
{columnName: "category_id", columnType: "INTEGER", columnComment: "分类ID"},
|
||||||
|
{columnName: "created_at", columnType: "TIMESTAMP", defaultValue: "NOW()", columnComment: "创建时间"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
回复: AI 已生成表结构设计:
|
||||||
|
表名: products (商品表)
|
||||||
|
字段:
|
||||||
|
- id (SERIAL, 主键, 自增) - 主键ID
|
||||||
|
- name (VARCHAR(200), 非空) - 商品名称
|
||||||
|
- price (DECIMAL(10)) - 价格
|
||||||
|
- stock (INTEGER) - 库存数量
|
||||||
|
- category_id (INTEGER) - 分类ID
|
||||||
|
- created_at (TIMESTAMP, 默认NOW()) - 创建时间
|
||||||
|
|
||||||
|
是否需要在此基础上修改或直接创建?
|
||||||
|
|
||||||
|
用户: "直接创建到 order_db 数据库"
|
||||||
|
|
||||||
|
调用: create_table(
|
||||||
|
connectionId="1",
|
||||||
|
databaseName="order_db",
|
||||||
|
tableName="products",
|
||||||
|
tableComment="商品表",
|
||||||
|
columns=[...] // 使用 AI 生成的 columns
|
||||||
|
)
|
||||||
|
回复: 已成功创建表 products (商品表)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- `requirement` 参数应尽可能详细,包含业务场景和字段需求
|
||||||
|
- AI 生成的表结构可能需要用户调整(如字段长度、类型)
|
||||||
|
- 创建表前需要确认 `connectionId` 和 `databaseName`
|
||||||
|
- 常用字段类型:`VARCHAR`, `INTEGER`, `SERIAL`(自增主键), `DECIMAL`, `TIMESTAMP`, `TEXT`, `BOOLEAN`
|
||||||
|
- 如果用户描述模糊,先引导用户补充细节
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 最佳实践
|
||||||
|
|
||||||
|
### 1. 始终确认环境
|
||||||
|
- 默认使用 `prod`(生产环境)
|
||||||
|
- 如果用户提到"测试"或"测试环境",使用 `target="test"`
|
||||||
|
- 执行危险操作前,明确告知用户当前环境
|
||||||
|
|
||||||
|
### 2. 提供上下文
|
||||||
|
- 展示数据时,包含字段名和类型
|
||||||
|
- 执行操作后,告知影响的行数或具体变化
|
||||||
|
- 错误时,提供清晰的错误信息和建议
|
||||||
|
|
||||||
|
### 3. 分步引导
|
||||||
|
- 如果用户请求不完整(如未指定数据源),先引导补充信息
|
||||||
|
- 复杂操作分步执行,每步确认后继续
|
||||||
|
- 对于多表操作,先展示整体计划
|
||||||
|
|
||||||
|
### 4. 数据展示
|
||||||
|
- 表格数据使用 Markdown 表格格式
|
||||||
|
- 长文本截断显示(最多 100 字符)
|
||||||
|
- 数字格式化(千位分隔符、货币符号)
|
||||||
|
- 时间格式化为可读格式
|
||||||
|
|
||||||
|
### 5. 错误处理
|
||||||
|
- API 错误:展示错误信息,建议重试或检查参数
|
||||||
|
- SQL 错误:展示 SQL 错误位置,建议修正
|
||||||
|
- 连接错误:检查数据源状态,建议启用或重新配置
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常用参数参考
|
||||||
|
|
||||||
|
### 数据源类型
|
||||||
|
- `builtin` - 内置 PostgreSQL
|
||||||
|
- `external` - 外部数据库(MySQL/PostgreSQL/Oracle/SQL Server 等)
|
||||||
|
|
||||||
|
### 数据源状态
|
||||||
|
- `0` - 运行中
|
||||||
|
- `1` - 已停止
|
||||||
|
|
||||||
|
### 环境
|
||||||
|
- `prod` - 生产环境(默认)
|
||||||
|
- `test` - 测试环境
|
||||||
|
|
||||||
|
### 常用字段类型
|
||||||
|
| 类型 | 用途 | 示例 |
|
||||||
|
|------|------|------|
|
||||||
|
| `SERIAL` | 自增主键 | `id SERIAL PRIMARY KEY` |
|
||||||
|
| `VARCHAR(n)` | 短文本 | `username VARCHAR(50)` |
|
||||||
|
| `TEXT` | 长文本 | `description TEXT` |
|
||||||
|
| `INTEGER` | 整数 | `stock INTEGER` |
|
||||||
|
| `DECIMAL(m,n)` | 精确小数 | `price DECIMAL(10,2)` |
|
||||||
|
| `TIMESTAMP` | 时间戳 | `created_at TIMESTAMP` |
|
||||||
|
| `BOOLEAN` | 布尔值 | `is_active BOOLEAN` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速开始
|
||||||
|
|
||||||
|
如果用户说"帮我查一下数据库",按以下步骤操作:
|
||||||
|
|
||||||
|
1. 调用 `list_datasources()` 获取数据源列表
|
||||||
|
2. 展示列表,让用户选择或默认第一个运行中的数据源
|
||||||
|
3. 调用 `get_datasource_detail(datasourceId="xx")` 获取数据库和表信息
|
||||||
|
4. 引导用户选择要操作的表
|
||||||
|
5. 根据用户意图调用相应的工具(查询/执行 SQL/增删改)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 实施步骤
|
||||||
|
|
||||||
|
1. 创建目录 `.kilo/skills/lzwcai-agile-db/`
|
||||||
|
2. 创建文件 `SKILL.md` 并写入上述内容
|
||||||
|
3. 验证 skill 文件格式正确
|
||||||
|
|
||||||
|
## 验证
|
||||||
|
|
||||||
|
创建完成后,skill 应该可以被 Kilo 自动识别和加载。
|
||||||
BIN
.kilo/skills/lzwcai-agile-db.zip
Normal file
BIN
.kilo/skills/lzwcai-agile-db.zip
Normal file
Binary file not shown.
1219
.kilo/skills/lzwcai-agile-db/SKILL.md
Normal file
1219
.kilo/skills/lzwcai-agile-db/SKILL.md
Normal file
File diff suppressed because it is too large
Load Diff
356
.kilo/skills/mcp-tool-testing/SKILL.md
Normal file
356
.kilo/skills/mcp-tool-testing/SKILL.md
Normal file
@@ -0,0 +1,356 @@
|
|||||||
|
---
|
||||||
|
name: mcp-tool-testing
|
||||||
|
description: MCP 工具通用测试技能。自动发现、执行和组合测试任何 MCP 服务器中的工具,支持场景自动创造和风险前置询问。
|
||||||
|
version: 0.1.0
|
||||||
|
---
|
||||||
|
|
||||||
|
# MCP Tool Testing Skill
|
||||||
|
|
||||||
|
这是一个通用的 MCP(Model Context Protocol)工具测试技能。适用于任何 MCP 环境,无论是自研的还是第三方的。
|
||||||
|
|
||||||
|
## 核心原则
|
||||||
|
|
||||||
|
1. **通用性**:不假设任何具体的服务器名称、工具名称或项目结构
|
||||||
|
2. **自动发现**:通过 MCP 协议本身获取当前环境中可用的所有工具
|
||||||
|
3. **智能执行**:能判断的入参自动执行,不能判断的询问用户
|
||||||
|
4. **场景自动创造**:根据工具自动匹配并创造测试场景,无需用户指定
|
||||||
|
5. **风险前置**:有风险的操作必须询问用户确认,绝不擅作主张
|
||||||
|
6. **结果汇总**:最终只返回执行工具的入参、出参和理解
|
||||||
|
|
||||||
|
## 执行流程
|
||||||
|
|
||||||
|
### 第一步:发现可用工具
|
||||||
|
|
||||||
|
通过 MCP 的 `tools/list` 方法获取当前环境中所有可用的工具。
|
||||||
|
|
||||||
|
**获取每个工具的信息:**
|
||||||
|
- 工具名称(name)
|
||||||
|
- 工具描述(description)
|
||||||
|
- 入参 schema(inputSchema):
|
||||||
|
- 参数名称
|
||||||
|
- 参数类型(string, integer, boolean, object, array, enum 等)
|
||||||
|
- 是否必填(required)
|
||||||
|
- 默认值(default)
|
||||||
|
- 枚举值(enum)
|
||||||
|
- 参数描述(description)
|
||||||
|
|
||||||
|
### 第二步:分析工具并分类
|
||||||
|
|
||||||
|
根据工具名称和描述,自动对工具进行分类:
|
||||||
|
|
||||||
|
1. **查询类**:list_xxx, get_xxx, query_xxx, search_xxx, find_xxx
|
||||||
|
2. **创建类**:create_xxx, add_xxx, insert_xxx, new_xxx
|
||||||
|
3. **更新类**:update_xxx, edit_xxx, modify_xxx, alter_xxx
|
||||||
|
4. **删除类**:delete_xxx, remove_xxx, drop_xxx
|
||||||
|
5. **执行类**:execute_xxx, run_xxx, call_xxx, invoke_xxx
|
||||||
|
6. **状态类**:toggle_xxx, enable_xxx, disable_xxx, start_xxx, stop_xxx
|
||||||
|
7. **其他**:无法归类的工具
|
||||||
|
|
||||||
|
### 第三步:执行单个工具
|
||||||
|
|
||||||
|
对每个工具,按以下策略执行:
|
||||||
|
|
||||||
|
#### 入参判断逻辑
|
||||||
|
|
||||||
|
```
|
||||||
|
对于每个必填参数(required 中的参数):
|
||||||
|
1. schema 有 default 值 → 使用该默认值
|
||||||
|
2. schema 有 enum 且只有一个值 → 使用该值
|
||||||
|
3. schema 有 enum 且有多个值 → 使用第一个,或询问用户
|
||||||
|
4. 参数名是常见的通用类型:
|
||||||
|
- pageNum, page, offset → 使用 1
|
||||||
|
- pageSize, limit, size → 使用 10
|
||||||
|
- target, environment → 使用 "prod" 或第一个 enum 值
|
||||||
|
- status, enabled → 使用 0 或 true
|
||||||
|
- name, title → 使用测试名称如 "test_item"
|
||||||
|
- id, xxxId → 需要从其他工具查询获取,或询问用户
|
||||||
|
5. 可以从之前执行的工具结果中获取 → 使用前一个工具的输出
|
||||||
|
6. 以上都不满足 → 询问用户
|
||||||
|
|
||||||
|
对于非必填参数:
|
||||||
|
- 一般不传,使用服务器默认值
|
||||||
|
- 如果需要测试特定功能,可以传一个合理值
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 询问用户的标准
|
||||||
|
|
||||||
|
当遇到以下情况时必须询问:
|
||||||
|
|
||||||
|
**1. 参数不明确**
|
||||||
|
- 参数是必填的,且值完全不明确
|
||||||
|
- 例如:某个特定的业务 ID、密钥、路径等
|
||||||
|
|
||||||
|
**2. 参数有多个合理选项且无法自动判断**
|
||||||
|
- 例如:enum 有 ["mysql", "postgresql", "oracle"],不知道测试用哪个
|
||||||
|
|
||||||
|
**3. 参数涉及敏感信息**
|
||||||
|
- 例如:password, token, apiKey, secret 等
|
||||||
|
|
||||||
|
**4. 高风险操作必须询问(绝不擅作主张)**
|
||||||
|
|
||||||
|
以下类型的工具/操作在执行前必须先询问用户确认:
|
||||||
|
|
||||||
|
- **删除类**:delete_xxx, remove_xxx, drop_xxx, destroy_xxx
|
||||||
|
- 风险:数据丢失,不可恢复
|
||||||
|
- 询问内容:确认是否要执行删除操作,指定删除目标或使用测试数据
|
||||||
|
|
||||||
|
- **泄密风险类**:export_xxx, dump_xxx, download_xxx, get_all_xxx(大量数据)
|
||||||
|
- 风险:可能导出敏感业务数据、用户信息、密钥等
|
||||||
|
- 询问内容:确认是否要导出数据,是否使用脱敏测试数据
|
||||||
|
|
||||||
|
- **政治敏感类**:涉及政府、政策、领导人等相关内容的工具
|
||||||
|
- 风险:可能触及政治敏感话题
|
||||||
|
- 询问内容:确认测试方向和范围
|
||||||
|
|
||||||
|
- **似黄/色情风险类**:涉及用户生成内容、图片、文本审核等相关工具
|
||||||
|
- 风险:可能接触到不当内容
|
||||||
|
- 询问内容:确认是否使用安全的测试数据
|
||||||
|
|
||||||
|
- **生产环境操作类**:影响生产环境数据的工具
|
||||||
|
- 风险:可能影响真实业务
|
||||||
|
- 询问内容:确认是否在测试环境执行,或使用只读模式
|
||||||
|
|
||||||
|
**5. 更新/修改类操作需要确认**
|
||||||
|
- 例如:update_xxx, modify_xxx 可能改变真实数据
|
||||||
|
|
||||||
|
**高风险操作询问格式:**
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 高风险操作提醒
|
||||||
|
|
||||||
|
工具:[工具名]
|
||||||
|
风险类型:[删除/泄密/政治敏感/似黄/生产环境影响]
|
||||||
|
具体风险:[描述可能的风险]
|
||||||
|
|
||||||
|
请选择:
|
||||||
|
1. 确认执行,使用测试数据
|
||||||
|
2. 确认执行,使用真实数据(我知道风险)
|
||||||
|
3. 跳过此工具
|
||||||
|
4. 其他指示
|
||||||
|
```
|
||||||
|
|
||||||
|
**普通询问格式:**
|
||||||
|
|
||||||
|
```
|
||||||
|
工具:[工具名]
|
||||||
|
参数:[参数名](类型:[类型],必填:是/否)
|
||||||
|
用途:[从 description 中提取]
|
||||||
|
|
||||||
|
请选择:
|
||||||
|
1. [建议选项1,如果有]
|
||||||
|
2. [建议选项2,如果有]
|
||||||
|
3. 提供自定义值
|
||||||
|
|
||||||
|
或者告诉我使用什么值。
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 执行顺序策略
|
||||||
|
|
||||||
|
1. **先执行无参数或少参数的查询工具**
|
||||||
|
- 如 list_xxx、get_all_xxx 等
|
||||||
|
- 这些工具通常不需要太多参数,可以直接执行
|
||||||
|
- 执行结果可以为后续工具提供 ID 等参数
|
||||||
|
|
||||||
|
2. **利用查询结果作为后续工具的入参**
|
||||||
|
- 例如:list_datasources 返回的 id 用于 get_datasource_detail
|
||||||
|
- 例如:list_tables 返回的 tableId 用于 query_table_data
|
||||||
|
|
||||||
|
3. **最后执行创建/修改/删除类工具**
|
||||||
|
- 这些通常需要更多上下文参数
|
||||||
|
- 删除操作要特别小心,确认是测试数据
|
||||||
|
|
||||||
|
### 第四步:场景自动创造与搭配测试
|
||||||
|
|
||||||
|
这是本技能的核心能力:根据当前所有可用工具,自动创造有意义的测试场景,无需用户指定。
|
||||||
|
|
||||||
|
#### 场景识别规则
|
||||||
|
|
||||||
|
根据工具的名称、描述和参数,自动匹配以下 8 种场景模式:
|
||||||
|
|
||||||
|
**1. 查询链路场景**
|
||||||
|
- 匹配规则:存在 list_xxx + get_xxx_detail/xxx_detail + query/get_data 类工具
|
||||||
|
- 创造方式:list 获取列表 → 取第一条的 ID → get detail 获取详情 → 用详情中的 ID 查询关联数据
|
||||||
|
- 示例:list_datasources → get_datasource_detail(id) → list_tables(datasourceId) → get_table_detail(tableId)
|
||||||
|
|
||||||
|
**2. 完整 CRUD 场景**
|
||||||
|
- 匹配规则:存在 create_xxx + list/query_xxx + update_xxx + delete_xxx 类工具
|
||||||
|
- 创造方式:create 创建测试数据 → list 确认存在 → update 修改 → list 确认修改 → delete 删除 → list 确认删除
|
||||||
|
- 示例:create_api_key → list_api_keys → toggle_api_key_status → list_api_keys → delete_api_key → list_api_keys
|
||||||
|
|
||||||
|
**3. 状态变更验证场景**
|
||||||
|
- 匹配规则:存在 get/list_xxx + toggle/enable/disable/start/stop_xxx 类工具
|
||||||
|
- 创造方式:get 初始状态 → toggle 变更状态 → get 验证状态已变更 → toggle 恢复 → get 验证恢复
|
||||||
|
- 示例:get_datasource_detail → toggle_datasource_status(status=1) → get_datasource_detail → toggle_datasource_status(status=0) → get_datasource_detail
|
||||||
|
|
||||||
|
**4. 参数传递链路场景**
|
||||||
|
- 匹配规则:工具 A 的输出字段与工具 B 的输入参数名匹配或语义相关
|
||||||
|
- 创造方式:执行 A → 从结果提取字段 → 作为 B 的输入 → 执行 B → 继续传递给 C
|
||||||
|
- 示例:list_datasources 返回 datasourceId → get_datasource_detail(datasourceId) 返回 connectionId → execute_sql(connectionId, sql)
|
||||||
|
|
||||||
|
**5. 批量操作场景**
|
||||||
|
- 匹配规则:存在接受 array 类型参数的工具(如批量删除、批量授权)
|
||||||
|
- 创造方式:list 获取多条记录 → 提取 IDs 数组 → 传递给批量操作工具 → 验证结果
|
||||||
|
- 示例:list_api_keys → 提取 ids → grant_api_key_permissions(batchDatas=[{...}])
|
||||||
|
|
||||||
|
**6. 条件分支场景**
|
||||||
|
- 匹配规则:工具支持不同枚举参数或可选参数,产生不同行为
|
||||||
|
- 创造方式:同一工具传不同参数 → 对比输出差异
|
||||||
|
- 示例:query_table_data(tableId, target="prod") vs query_table_data(tableId, target="test")
|
||||||
|
|
||||||
|
**7. 异常处理场景**
|
||||||
|
- 匹配规则:任何工具
|
||||||
|
- 创造方式:传入无效 ID、空参数、错误类型 → 验证错误处理是否合理
|
||||||
|
- 示例:get_datasource_detail(datasourceId="invalid") → 验证返回错误信息
|
||||||
|
|
||||||
|
**8. 跨服务器场景**(当有多个 MCP 服务器时)
|
||||||
|
- 匹配规则:不同服务器的工具之间存在业务关联
|
||||||
|
- 创造方式:服务器A的输出 → 作为服务器B的输入
|
||||||
|
- 示例:IoT 获取设备列表 → Dify workflow 处理设备数据 → SQL 执行存储结果
|
||||||
|
|
||||||
|
#### 场景自动创造流程
|
||||||
|
|
||||||
|
```
|
||||||
|
1. 收集所有工具,建立工具字典
|
||||||
|
- key: 工具名
|
||||||
|
- value: {description, inputSchema, outputSample, category}
|
||||||
|
|
||||||
|
2. 构建参数依赖图
|
||||||
|
- 分析每个工具的输入参数名(如 datasourceId, tableId, apiKeyId)
|
||||||
|
- 分析每个工具的输出字段(从执行结果中提取)
|
||||||
|
- 建立 "输出字段 → 输入参数" 的映射关系
|
||||||
|
|
||||||
|
3. 匹配场景模式
|
||||||
|
- 遍历上述 8 种场景规则
|
||||||
|
- 对每种规则,检查当前工具集是否满足匹配条件
|
||||||
|
- 满足则创造具体场景实例
|
||||||
|
|
||||||
|
4. 场景优先级排序
|
||||||
|
- P0: 查询链路(最基础,最先执行)
|
||||||
|
- P1: CRUD 完整链路(验证完整生命周期)
|
||||||
|
- P2: 状态变更验证(验证状态管理)
|
||||||
|
- P3: 参数传递链路(验证工具间协作)
|
||||||
|
- P4: 批量操作、条件分支、异常处理
|
||||||
|
- P5: 跨服务器场景(最复杂,最后执行)
|
||||||
|
|
||||||
|
5. 执行场景
|
||||||
|
- 按优先级依次执行
|
||||||
|
- 前一步的输出自动传递给下一步
|
||||||
|
- 任何一步失败则标记场景失败,继续下一个场景
|
||||||
|
- 场景中包含高风险操作时,执行前询问用户
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 场景执行记录格式
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
### 场景:[场景名称]
|
||||||
|
- **触发规则**:[匹配的场景模式,如 "查询链路场景"]
|
||||||
|
- **涉及工具**:工具A → 工具B → 工具C
|
||||||
|
- **执行过程**:
|
||||||
|
| 步骤 | 工具 | 入参 | 出参摘要 | 状态 |
|
||||||
|
|------|------|------|---------|------|
|
||||||
|
| 1 | list_xxx | {} | 返回 5 条记录 | ✅ |
|
||||||
|
| 2 | get_xxx | {id: "从步骤1获取"} | 返回详情 | ✅ |
|
||||||
|
| 3 | query_xxx | {xxxId: "从步骤2获取"} | 返回结果 | ✅ |
|
||||||
|
- **数据流**:步骤1.id → 步骤2.id → 步骤2.connectionId → 步骤3.connectionId
|
||||||
|
- **场景结果**:✅ 全部成功 / ❌ 步骤N失败(原因:...)
|
||||||
|
- **理解**:[对这个场景的整体理解,工具间如何协作]
|
||||||
|
```
|
||||||
|
|
||||||
|
**场景测试要点:**
|
||||||
|
- 记录完整的数据流转过程
|
||||||
|
- 验证工具之间的兼容性
|
||||||
|
- 检查数据格式是否一致
|
||||||
|
- 场景创造过程不需要用户干预,自动完成
|
||||||
|
- 只在遇到无法判断的参数或高风险操作时才询问用户
|
||||||
|
|
||||||
|
### 第五步:生成报告
|
||||||
|
|
||||||
|
最终只返回以下内容:
|
||||||
|
|
||||||
|
```markdown
|
||||||
|
# MCP 工具测试报告
|
||||||
|
|
||||||
|
## 工具列表
|
||||||
|
|
||||||
|
共发现 [N] 个工具:
|
||||||
|
|
||||||
|
| 序号 | 工具名 | 描述 | 必填参数 | 分类 |
|
||||||
|
|------|--------|------|---------|------|
|
||||||
|
|
||||||
|
## 工具执行结果
|
||||||
|
|
||||||
|
### 1. [工具名]
|
||||||
|
- **描述**:[工具描述]
|
||||||
|
- **入参**:`{JSON}`
|
||||||
|
- **出参**:`{JSON 摘要}`
|
||||||
|
- **状态**:✅ 成功 / ❌ 失败 / ⚠️ 跳过(原因:...)
|
||||||
|
- **理解**:[你对这个工具功能的简要理解]
|
||||||
|
|
||||||
|
### 2. [工具名]
|
||||||
|
...
|
||||||
|
|
||||||
|
## 场景测试
|
||||||
|
|
||||||
|
### 场景1:[场景名称]
|
||||||
|
- **流程**:工具A → 工具B → 工具C
|
||||||
|
- **数据流**:[描述数据如何在工具间传递]
|
||||||
|
- **结果**:[最终结果摘要]
|
||||||
|
- **状态**:✅ 成功 / ❌ 失败
|
||||||
|
|
||||||
|
## 总结
|
||||||
|
|
||||||
|
| 指标 | 数量 |
|
||||||
|
|------|------|
|
||||||
|
| 发现工具总数 | N |
|
||||||
|
| 成功执行 | N |
|
||||||
|
| 执行失败 | N |
|
||||||
|
| 跳过(需用户提供信息) | N |
|
||||||
|
| 高风险操作(已询问用户) | N |
|
||||||
|
| 场景测试数 | N |
|
||||||
|
|
||||||
|
## 待确认事项
|
||||||
|
|
||||||
|
列出需要用户提供的参数或信息:
|
||||||
|
1. [工具名] 的 [参数名]:[说明]
|
||||||
|
2. ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 特殊处理
|
||||||
|
|
||||||
|
### 动态工具
|
||||||
|
|
||||||
|
有些服务器启动时从外部加载工具定义(如从 API、配置文件、工作流引擎等):
|
||||||
|
1. 通过 MCP 协议获取实际可用的工具列表
|
||||||
|
2. 对动态获取的工具同样按照上述流程测试
|
||||||
|
3. 记录工具的来源信息(如果有)
|
||||||
|
|
||||||
|
### 需要认证/配置的服务器
|
||||||
|
|
||||||
|
某些 MCP 服务器需要特定的环境变量、API Key、Token 等:
|
||||||
|
1. 检查服务器是否正常运行
|
||||||
|
2. 如果启动失败,记录错误信息并跳过
|
||||||
|
3. 如果运行正常但工具调用失败(如 401),询问用户提供认证信息
|
||||||
|
|
||||||
|
### 失败处理
|
||||||
|
|
||||||
|
- **网络超时**:记录错误,标记为失败,继续下一个
|
||||||
|
- **参数错误**:检查是否需要补充参数,或询问用户
|
||||||
|
- **服务器未运行**:记录原因,跳过该服务器的所有工具
|
||||||
|
- **工具不存在**:可能工具定义已变更,重新获取工具列表
|
||||||
|
|
||||||
|
### 输出处理
|
||||||
|
|
||||||
|
- 输出过长时适当截断(如超过 2000 字符)
|
||||||
|
- 保留关键信息:状态码、主要数据、错误信息
|
||||||
|
- 对于列表类输出,显示前几条和总数
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
1. **保持参数一致性**:同一个 ID 在多个工具中保持相同
|
||||||
|
2. **风险前置**:高风险操作必须先询问用户,绝不擅作主张
|
||||||
|
3. **记录完整**:每个工具的入参、出参都要记录
|
||||||
|
4. **及时询问**:遇到不明确的参数不要猜测,询问用户
|
||||||
|
5. **场景优先**:优先测试有意义的场景组合,而不是孤立地测试每个工具
|
||||||
|
6. **自动创造**:场景不需要用户指定,根据工具自动匹配和创造
|
||||||
|
7. **最终输出**:只返回入参、出参和理解,不需要冗长的过程描述
|
||||||
|
8. **五类风险**:删除、泄密、政治、似黄、生产环境影响——必须询问用户
|
||||||
343
.kilo/skills/数字员工平台数据库技能的.md
Normal file
343
.kilo/skills/数字员工平台数据库技能的.md
Normal file
@@ -0,0 +1,343 @@
|
|||||||
|
# 数字员工平台数据库技能配置指南
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
本文档介绍两种使用数字员工平台数据库技能的方法:
|
||||||
|
- **方法一**:通过数字员工对话直接使用
|
||||||
|
- **方法二**:通过 AI 编辑器的 Skills + MCP 搭配使用
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方法一:通过数字员工对话使用
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
此方法通过配置 MCP Server 来连接数字员工平台。
|
||||||
|
|
||||||
|
### MCP 配置示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"lzwcai-mcp-agile-db": {
|
||||||
|
"command": "uvx",
|
||||||
|
"type": "stdio",
|
||||||
|
"args": [
|
||||||
|
"lzwcai-mcp-agile-db"
|
||||||
|
],
|
||||||
|
"timeout": 600,
|
||||||
|
"env": {
|
||||||
|
"API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
- 配置中的 `API_KEY` 是数字员工平台的密钥
|
||||||
|
- 存在单点登录问题,请使用不会过期的账号密钥
|
||||||
|
- Skills 配置参考:`lzwcai-agile-db`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 方法二:通过 AI 编辑器使用
|
||||||
|
|
||||||
|
### 配置说明
|
||||||
|
|
||||||
|
此方法通过下载 Skills 和 MCP 配置,在 AI 编辑器中搭配使用,支持直接对话操作。
|
||||||
|
|
||||||
|
### MCP 配置示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"lzwcai_mcp_agile_db": {
|
||||||
|
"command": "uvx",
|
||||||
|
"args": [
|
||||||
|
"lzwcai-mcp-agile-db"
|
||||||
|
],
|
||||||
|
"env": {
|
||||||
|
"UV_INDEX_URL": "http://192.168.2.236:3141/lzwc/dev/+simple/",
|
||||||
|
"API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q",
|
||||||
|
"backendBaseUrl": "http://192.168.2.236:8088"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 环境变量说明
|
||||||
|
|
||||||
|
| 变量名 | 说明 | 示例值 |
|
||||||
|
|--------|------|--------|
|
||||||
|
| `UV_INDEX_URL` | Python 包索引地址 | `http://192.168.2.236:3141/lzwc/dev/+simple/` |
|
||||||
|
| `API_KEY` | 数字员工平台 API 密钥 | (见配置示例) |
|
||||||
|
| `backendBaseUrl` | 后端服务基础地址 | `http://192.168.2.236:8088` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用建议
|
||||||
|
|
||||||
|
1. **选择方法**:根据实际使用场景选择合适的方法
|
||||||
|
- 数字员工对话:适合快速使用
|
||||||
|
- AI 编辑器:适合更灵活的配置和使用
|
||||||
|
|
||||||
|
2. **密钥管理**:妥善保管 `API_KEY`,避免泄露
|
||||||
|
|
||||||
|
3. **网络配置**:确保网络可以访问配置的后端地址
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 相关资源
|
||||||
|
|
||||||
|
- Skills 目录:`lzwcai-agile-db/`
|
||||||
|
- 详细技能文档:`lzwcai-agile-db/SKILL.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 对话使用场景示例
|
||||||
|
|
||||||
|
以下是配置好技能后,您可以直接使用的对话示例:
|
||||||
|
|
||||||
|
### 📊 场景 1:浏览数据源和表
|
||||||
|
|
||||||
|
**用户:** "帮我看看有哪些数据源"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
共找到 3 个数据源:
|
||||||
|
1. HMD产品(PostgreSQL, 运行中)
|
||||||
|
2. 测试数据库(MySQL, 运行中)
|
||||||
|
3. 历史数据(Oracle, 已停止)
|
||||||
|
|
||||||
|
请告诉我您想看哪个数据源?
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**用户:** "看看HMD产品有哪些表"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
HMD产品 数据源包含以下数据库和表:
|
||||||
|
|
||||||
|
order_db:
|
||||||
|
- orders (订单表, 15 个字段, 2568 条记录)
|
||||||
|
- users (用户表, 8 个字段, 156 条记录)
|
||||||
|
- products (商品表, 12 个字段, 892 条记录)
|
||||||
|
|
||||||
|
analytics_db:
|
||||||
|
- daily_stats (日报表, 10 个字段, 365 条记录)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔍 场景 2:查询表数据
|
||||||
|
|
||||||
|
**用户:** "查一下users表前10条数据"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
users 表共 156 条记录,当前显示第 1-10 条:
|
||||||
|
|
||||||
|
| id | username | email | status | created_at |
|
||||||
|
|----|----------|-----------------|--------|---------------------|
|
||||||
|
| 1 | admin | admin@test.com | active | 2024-01-01 00:00:00 |
|
||||||
|
| 2 | user1 | user1@test.com | active | 2024-01-02 10:30:00 |
|
||||||
|
| 3 | user2 | user2@test.com | inactive| 2024-01-03 15:45:00 |
|
||||||
|
...
|
||||||
|
|
||||||
|
注:默认查询测试环境,如需查询生产环境请说明。
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📝 场景 3:执行 SQL 查询
|
||||||
|
|
||||||
|
**用户:** "统计每个地区的订单数量"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
各地区订单统计:
|
||||||
|
|
||||||
|
| 地区 | 订单数 |
|
||||||
|
|------|--------|
|
||||||
|
| 华东 | 1,250 |
|
||||||
|
| 华南 | 980 |
|
||||||
|
| 华北 | 756 |
|
||||||
|
| 西南 | 432 |
|
||||||
|
| 东北 | 289 |
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ➕ 场景 4:插入数据
|
||||||
|
|
||||||
|
**用户:** "新增一个用户,用户名是test_user,邮箱test@test.com"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
即将向 users 表插入以下数据:
|
||||||
|
username = test_user
|
||||||
|
email = test@test.com
|
||||||
|
(id 自动生成)
|
||||||
|
|
||||||
|
确认插入吗?
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户:** "确认"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
✅ 已成功插入用户 test_user (ID: 157)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✏️ 场景 5:更新数据
|
||||||
|
|
||||||
|
**用户:** "把ID为5的用户邮箱改成new@test.com"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
即将更新 users 表 ID=5 的记录:
|
||||||
|
email: old@test.com → new@test.com
|
||||||
|
|
||||||
|
确认更新吗?
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户:** "确认"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
✅ 已更新用户 ID=5 的邮箱为 new@test.com
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🗑️ 场景 6:删除数据
|
||||||
|
|
||||||
|
**用户:** "删除ID为10的订单"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
⚠️ 安全提醒:此操作存在删除风险。
|
||||||
|
具体说明:数据将永久删除,不可恢复。
|
||||||
|
|
||||||
|
确认要删除以下记录吗?
|
||||||
|
订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00
|
||||||
|
|
||||||
|
请回复"确认删除"继续。
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户:** "确认删除"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
✅ 已成功删除订单 ID=10
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🤖 场景 7:AI 生成表结构
|
||||||
|
|
||||||
|
**用户:** "帮我设计一个商品表,包含名称、价格、库存、分类"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
AI 已生成表结构设计:
|
||||||
|
|
||||||
|
表名: products (商品表)
|
||||||
|
字段:
|
||||||
|
- id (SERIAL, 主键) - 主键ID
|
||||||
|
- name (VARCHAR(200), 非空) - 商品名称
|
||||||
|
- price (DECIMAL(10,2)) - 价格
|
||||||
|
- stock (INTEGER) - 库存数量
|
||||||
|
- category (VARCHAR(50)) - 分类
|
||||||
|
- created_at (TIMESTAMP) - 创建时间
|
||||||
|
|
||||||
|
是否需要在此基础上修改或直接创建?
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户:** "直接创建到order_db数据库"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
✅ 已成功创建表 products (商品表)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📥 场景 8:导入 Excel 数据
|
||||||
|
|
||||||
|
**用户:** "帮我导入这个Excel文件:https://example.com/data.xlsx"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
AI 已识别 Excel 文件内容:
|
||||||
|
|
||||||
|
表名: customers (客户表)
|
||||||
|
字段:
|
||||||
|
- id (SERIAL, 主键)
|
||||||
|
- name (VARCHAR(100)) - 客户名称
|
||||||
|
- phone (VARCHAR(20)) - 电话
|
||||||
|
- address (TEXT) - 地址
|
||||||
|
|
||||||
|
预览数据(前3条):
|
||||||
|
| id | name | phone | address |
|
||||||
|
|----|--------|------------|----------------|
|
||||||
|
| 1 | 张三 | 13800138000| 北京市朝阳区 |
|
||||||
|
| 2 | 李四 | 13900139000| 上海市浦东新区 |
|
||||||
|
| 3 | 王五 | 13700137000| 广州市天河区 |
|
||||||
|
|
||||||
|
共 50 条数据待导入。
|
||||||
|
|
||||||
|
⚠️ 安全提醒:此操作可能涉及数据安全风险。
|
||||||
|
说明:导入的数据将写入数据库,请确认数据来源合法合规。
|
||||||
|
请确认是否继续?
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户:** "继续"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
✅ 成功导入 50 条数据到 customers 表
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔑 场景 9:API 密钥管理
|
||||||
|
|
||||||
|
**用户:** "帮我创建一个新的API密钥"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
请输入密钥名称:
|
||||||
|
```
|
||||||
|
|
||||||
|
**用户:** "测试密钥"
|
||||||
|
|
||||||
|
**AI 回复:**
|
||||||
|
```
|
||||||
|
✅ 已创建 API 密钥:
|
||||||
|
名称: 测试密钥
|
||||||
|
密钥: lzwc_sk_xxxxxxxxxxxx
|
||||||
|
状态: 启用
|
||||||
|
过期时间: 2025-06-18
|
||||||
|
|
||||||
|
请妥善保管密钥,丢失后无法找回。
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ⚠️ 重要使用提示
|
||||||
|
|
||||||
|
1. **环境选择:** 默认使用测试环境(`test`),操作生产环境需要明确说明"查询生产环境"或"操作生产环境"
|
||||||
|
|
||||||
|
2. **安全确认:** 所有写操作(增删改)都会先预览并等待您的确认
|
||||||
|
|
||||||
|
3. **多选原则:** 有多个数据源/数据库/表可选时,AI会列出让您选择,不会擅自做主
|
||||||
|
|
||||||
|
4. **分步执行:** 复杂任务会分步完成,每一步都会确认后再继续
|
||||||
86
lzwcai_mcp_agile_db/README.md
Normal file
86
lzwcai_mcp_agile_db/README.md
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
# lzwcai-mcp-agile-db
|
||||||
|
|
||||||
|
数据库管理平台 MCP Server,提供 34 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
| 变量名 | 必填 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `API_KEY` | 是 | 数据库管理平台的 API 密钥(格式: `Bearer <token>`) |
|
||||||
|
| `backendBaseUrl` | 否 | 数据库管理平台后端地址(默认 `http://lzwcai-demp-corp-manager:8086`) |
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## 运行
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 设置环境变量
|
||||||
|
export API_KEY="Bearer your-token"
|
||||||
|
export backendBaseUrl="https://dempdemo.lzwcai.com" # 可选
|
||||||
|
|
||||||
|
# 运行 MCP Server
|
||||||
|
lzwcai-mcp-agile-db
|
||||||
|
```
|
||||||
|
|
||||||
|
## 工具列表
|
||||||
|
|
||||||
|
### 数据源管理
|
||||||
|
- `list_datasources` - 获取数据源列表
|
||||||
|
- `get_datasource_detail` - 获取数据源详情
|
||||||
|
- `create_datasource` - 创建数据源
|
||||||
|
- `update_datasource` - 更新数据源
|
||||||
|
- `toggle_datasource_status` - 启用/停用数据源
|
||||||
|
- `delete_datasource` - 删除数据源
|
||||||
|
|
||||||
|
### 数据库与表管理
|
||||||
|
- `list_databases` - 获取数据库列表
|
||||||
|
- `list_tables` - 获取表列表
|
||||||
|
- `get_table_detail` - 获取表详情
|
||||||
|
- `create_table` - 创建表
|
||||||
|
- `alter_table` - 修改表结构
|
||||||
|
- `generate_table_by_description` - 通过自然语言生成表结构
|
||||||
|
|
||||||
|
### 表数据 CRUD
|
||||||
|
- `query_table_data` - 查询表数据
|
||||||
|
- `insert_table_row` - 插入行数据
|
||||||
|
- `update_table_row` - 更新行数据
|
||||||
|
- `delete_table_rows` - 删除行数据
|
||||||
|
- `export_table_excel` - 导出 Excel
|
||||||
|
|
||||||
|
### API 密钥管理
|
||||||
|
- `list_api_keys` - 获取密钥列表
|
||||||
|
- `create_api_key` - 创建密钥
|
||||||
|
- `toggle_api_key_status` - 启用/禁用密钥
|
||||||
|
- `delete_api_key` - 删除密钥
|
||||||
|
- `get_api_key_permissions` - 查看密钥权限
|
||||||
|
- `grant_api_key_permissions` - 授予权限(仅追加,不可撤销)
|
||||||
|
|
||||||
|
### 技能与工具管理
|
||||||
|
- `add_sql_tool_to_datasource` - 把 SQL 沉淀为工具(一步到位,自动建技能+配模板+建工具,推荐入口)
|
||||||
|
- `get_skill_by_datasource` - 获取技能信息
|
||||||
|
- `get_skill_tools` - 获取技能工具列表
|
||||||
|
- `create_sql_tool` - 创建 SQL 工具(需技能已存在)
|
||||||
|
- `delete_skill_tool` - 删除技能工具
|
||||||
|
- `update_skill_config` - 更新技能配置
|
||||||
|
- `update_skill_tool` - 修改技能工具
|
||||||
|
|
||||||
|
### 数据导入
|
||||||
|
- `preview_import_data` - 预览导入数据
|
||||||
|
- `confirm_import_data` - 确认导入数据
|
||||||
|
|
||||||
|
### 表订阅与 SQL 执行
|
||||||
|
- `toggle_table_subscription` - 切换表订阅
|
||||||
|
- `execute_sql` - 执行 SQL 查询
|
||||||
|
|
||||||
|
## 架构
|
||||||
|
|
||||||
|
- `tools/_base.py` - 工具注册装饰器和基类
|
||||||
|
- `tools/*.py` - 工具实现文件
|
||||||
|
- `utils/api_client.py` - 统一 HTTP 客户端
|
||||||
|
- `utils/env_config.py` - 环境变量配置
|
||||||
|
- `utils/logger_config.py` - 日志配置
|
||||||
|
- `server.py` - MCP Server 注册和启动逻辑
|
||||||
1
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/.python-version
Normal file
1
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
0
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/README.md
Normal file
0
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/README.md
Normal file
5
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__init__.py
Normal file
5
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
"""lzwcai-mcp-agile-db MCP Server 包"""
|
||||||
|
|
||||||
|
from .server import main
|
||||||
|
|
||||||
|
__all__ = ["main"]
|
||||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,377 @@
|
|||||||
|
2026-06-22 23:02:48 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||||
|
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||||
|
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||||
|
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||||
|
2026-06-22 23:02:48 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||||
|
2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:73] - [客户端初始化] base_url=https://dempdemo.lzwcai.com/api, 认证方式=account:demp04
|
||||||
|
2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:118] - [登录] POST https://dempdemo.lzwcai.com/api/login, username=demp04, loginType=user
|
||||||
|
2026-06-22 23:02:48 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
||||||
|
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12F4F5C0>
|
||||||
|
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000020F12F165D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
||||||
|
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12DFD970>
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Mon, 22 Jun 2026 15:02:58 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
||||||
|
2026-06-22 23:02:49 - httpx - INFO - [_client.py:1740] - HTTP Request: POST https://dempdemo.lzwcai.com/api/login "HTTP/1.1 200 "
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:161] - [API响应] HTTP 200
|
||||||
|
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:137] - [登录] 成功获取 token
|
||||||
|
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||||
|
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||||
|
2026-06-23 09:47:20 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||||
|
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||||
|
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||||
|
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||||
|
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
|
||||||
|
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
|
||||||
|
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
|
||||||
|
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
|
||||||
|
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
|
||||||
|
2026-06-23 09:47:20 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||||
|
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||||
|
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB167DF860>
|
||||||
|
2026-06-23 09:47:21 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||||
|
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||||
|
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||||
|
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8088, 认证方式=account:yy8z9
|
||||||
|
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
|
||||||
|
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||||
|
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB156EB260>
|
||||||
|
2026-06-23 09:47:28 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||||
|
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||||
|
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||||
|
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8088/login, username=yy8z9, loginType=user
|
||||||
|
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None
|
||||||
|
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001FB16D14260>
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
|
||||||
|
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8088/login "HTTP/1.1 200 "
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||||
|
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||||
|
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8088/datasource/api_key/list
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
|
||||||
|
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8088/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||||
|
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||||
|
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||||
|
"total": 21,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"createBy": "",
|
||||||
|
"createTime": "2026-06-18 11:29:46",
|
||||||
|
"updateBy": "",
|
||||||
|
"updateTime": "2026-06-18 11:59:25",
|
||||||
|
"remark": null,
|
||||||
|
"id": "37",
|
||||||
|
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||||
|
"apiKeyName": "盒马超市只读访问密钥",
|
||||||
|
"enterpriseId": "1932095424144715777",
|
||||||
|
"status": 0,
|
||||||
|
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"createBy": "",
|
||||||
|
"createTime": "2026-06-18 10:54:40",
|
||||||
|
...
|
||||||
|
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||||
|
2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||||
|
2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||||
|
2026-06-23 09:48:33 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||||
|
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||||
|
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||||
|
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||||
|
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
|
||||||
|
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
|
||||||
|
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
|
||||||
|
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
|
||||||
|
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
|
||||||
|
2026-06-23 09:48:33 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||||
|
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||||
|
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001F89A81AAB0>
|
||||||
|
2026-06-23 09:48:34 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||||
|
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||||
|
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||||
|
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
|
||||||
|
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
|
||||||
|
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||||
|
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001F89A273B00>
|
||||||
|
2026-06-23 09:48:36 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||||
|
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||||
|
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||||
|
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
|
||||||
|
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
|
||||||
|
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001F89C2B8F20>
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||||
|
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||||
|
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||||
|
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||||
|
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||||
|
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||||
|
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||||
|
"total": 21,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"createBy": "",
|
||||||
|
"createTime": "2026-06-18 11:29:46",
|
||||||
|
"updateBy": "",
|
||||||
|
"updateTime": "2026-06-18 11:59:25",
|
||||||
|
"remark": null,
|
||||||
|
"id": "37",
|
||||||
|
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||||
|
"apiKeyName": "盒马超市只读访问密钥",
|
||||||
|
"enterpriseId": "1932095424144715777",
|
||||||
|
"status": 0,
|
||||||
|
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"createBy": "",
|
||||||
|
"createTime": "2026-06-18 10:54:40",
|
||||||
|
...
|
||||||
|
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||||
|
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||||
|
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||||
|
2026-06-23 11:11:10 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||||
|
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||||
|
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||||
|
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||||
|
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
|
||||||
|
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
|
||||||
|
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
|
||||||
|
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
|
||||||
|
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
|
||||||
|
2026-06-23 11:11:10 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||||
|
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||||
|
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCAFE606E0>
|
||||||
|
2026-06-23 11:11:11 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||||
|
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||||
|
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||||
|
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
|
||||||
|
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
|
||||||
|
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||||
|
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0FBFB60>
|
||||||
|
2026-06-23 11:11:14 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||||
|
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||||
|
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||||
|
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
|
||||||
|
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
|
||||||
|
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB15974D0>
|
||||||
|
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||||
|
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||||
|
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||||
|
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||||
|
"total": 21,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"createBy": "",
|
||||||
|
"createTime": "2026-06-18 11:29:46",
|
||||||
|
"updateBy": "",
|
||||||
|
"updateTime": "2026-06-18 11:59:25",
|
||||||
|
"remark": null,
|
||||||
|
"id": "37",
|
||||||
|
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||||
|
"apiKeyName": "盒马超市只读访问密钥",
|
||||||
|
"enterpriseId": "1932095424144715777",
|
||||||
|
"status": 0,
|
||||||
|
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"createBy": "",
|
||||||
|
"createTime": "2026-06-18 10:54:40",
|
||||||
|
...
|
||||||
|
2026-06-23 11:11:15 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||||
|
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0EA5D60>
|
||||||
|
2026-06-23 11:11:27 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||||
|
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||||
|
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||||
|
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
|
||||||
|
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||||
|
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
|
||||||
|
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB100F1D0>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=utf-8'), (b'Content-Length', b'51'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN')])
|
||||||
|
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:239] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||||
|
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||||
|
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||||
|
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||||
|
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||||
|
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||||
|
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||||
|
"total": 21,
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"createBy": "",
|
||||||
|
"createTime": "2026-06-18 11:29:46",
|
||||||
|
"updateBy": "",
|
||||||
|
"updateTime": "2026-06-18 11:59:25",
|
||||||
|
"remark": null,
|
||||||
|
"id": "37",
|
||||||
|
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||||
|
"apiKeyName": "盒马超市只读访问密钥",
|
||||||
|
"enterpriseId": "1932095424144715777",
|
||||||
|
"status": 0,
|
||||||
|
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"createBy": "",
|
||||||
|
"createTime": "2026-06-18 10:54:40",
|
||||||
|
...
|
||||||
|
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||||
|
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||||
|
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||||
|
2026-06-23 11:37:08 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||||
|
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||||
|
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||||
|
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||||
|
2026-06-23 11:37:08 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401
|
||||||
143
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py
Normal file
143
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
lzwcai-mcp-agile-db MCP Server
|
||||||
|
数据库管理平台 MCP 工具服务,提供数据库管理、表/数据操作、技能、API 密钥、MQTT 同步、AI 训练等工具
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import anyio
|
||||||
|
|
||||||
|
import mcp.types as types
|
||||||
|
from mcp.server import NotificationOptions, Server
|
||||||
|
from mcp.server.models import InitializationOptions
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
|
|
||||||
|
from .utils.logger_config import setup_system_logging, get_logger
|
||||||
|
from .utils.api_client import AgileDBAPIClient
|
||||||
|
from .tools._base import get_registered_tools
|
||||||
|
|
||||||
|
# 初始化日志系统
|
||||||
|
setup_system_logging(app_name="lzwcai_mcp_agile_db", log_level=logging.DEBUG)
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# 初始化 MCP Server
|
||||||
|
server = Server("lzwcai_mcp_agile_db")
|
||||||
|
|
||||||
|
# 全局 API 客户端
|
||||||
|
_api_client: AgileDBAPIClient = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_client() -> AgileDBAPIClient:
|
||||||
|
"""获取或创建 API 客户端"""
|
||||||
|
global _api_client
|
||||||
|
if _api_client is None:
|
||||||
|
_api_client = AgileDBAPIClient()
|
||||||
|
return _api_client
|
||||||
|
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def handle_list_tools() -> list[types.Tool]:
|
||||||
|
"""列出所有可用工具"""
|
||||||
|
logger.info("收到 ListTools 请求")
|
||||||
|
|
||||||
|
tools = []
|
||||||
|
for tool_cls in get_registered_tools():
|
||||||
|
instance = tool_cls(get_api_client())
|
||||||
|
tool_def = instance.to_tool_def()
|
||||||
|
tools.append(
|
||||||
|
types.Tool(
|
||||||
|
name=tool_def["name"],
|
||||||
|
description=tool_def["description"],
|
||||||
|
inputSchema=tool_def["inputSchema"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info(f"ListTools 响应: 返回 {len(tools)} 个工具")
|
||||||
|
return tools
|
||||||
|
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
async def handle_call_tool(
|
||||||
|
name: str,
|
||||||
|
arguments: dict | None,
|
||||||
|
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
|
||||||
|
"""调用工具"""
|
||||||
|
logger.info(f"收到 CallTool 请求: name={name}")
|
||||||
|
|
||||||
|
# 查找对应的工具类
|
||||||
|
tool_cls = None
|
||||||
|
for cls in get_registered_tools():
|
||||||
|
if cls.name == name:
|
||||||
|
tool_cls = cls
|
||||||
|
break
|
||||||
|
|
||||||
|
if tool_cls is None:
|
||||||
|
logger.error(f"未找到工具: {name}")
|
||||||
|
raise ValueError(f"未知工具: {name}")
|
||||||
|
|
||||||
|
# 创建工具实例并执行
|
||||||
|
client = get_api_client()
|
||||||
|
tool_instance = tool_cls(client)
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = await tool_instance.execute(arguments or {})
|
||||||
|
|
||||||
|
logger.info(f"工具执行成功: {name}")
|
||||||
|
logger.debug(f"工具返回结果: {json.dumps(result, ensure_ascii=False, indent=2)[:500]}...")
|
||||||
|
|
||||||
|
return [
|
||||||
|
types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(result, ensure_ascii=False, indent=2),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"工具执行失败: {name}, 错误: {e}", exc_info=True)
|
||||||
|
return [
|
||||||
|
types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps({"error": str(e), "tool_name": name}, ensure_ascii=False, indent=2),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def run_server():
|
||||||
|
"""运行 MCP Server (stdio 模式)"""
|
||||||
|
try:
|
||||||
|
async with stdio_server() as streams:
|
||||||
|
await server.run(
|
||||||
|
streams[0],
|
||||||
|
streams[1],
|
||||||
|
InitializationOptions(
|
||||||
|
server_name="lzwcai_mcp_agile_db",
|
||||||
|
server_version="0.1.8",
|
||||||
|
capabilities=server.get_capabilities(
|
||||||
|
notification_options=NotificationOptions(),
|
||||||
|
experimental_capabilities={},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
# 释放全局 HTTP 客户端
|
||||||
|
if _api_client is not None:
|
||||||
|
await _api_client.close()
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主入口"""
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info("lzwcai-mcp-agile-db MCP Server 启动")
|
||||||
|
|
||||||
|
# 导入所有工具模块(触发装饰器注册)
|
||||||
|
from . import tools # noqa: F401
|
||||||
|
|
||||||
|
logger.info(f"已注册工具数量: {len(get_registered_tools())}")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
|
||||||
|
logger.info("开始运行 MCP Server (stdio 模式)")
|
||||||
|
anyio.run(run_server)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
18
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__init__.py
Normal file
18
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__init__.py
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
"""
|
||||||
|
工具自动发现模块
|
||||||
|
自动导入 tools/ 目录下所有工具模块,触发 @register_tool 装饰器注册
|
||||||
|
"""
|
||||||
|
|
||||||
|
import importlib
|
||||||
|
import pkgutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
# 获取当前包路径
|
||||||
|
_package_path = Path(__file__).parent
|
||||||
|
|
||||||
|
# 遍历所有 Python 文件(排除 __init__.py 和 _base.py)
|
||||||
|
for _, module_name, _ in pkgutil.iter_modules([str(_package_path)]):
|
||||||
|
if module_name.startswith("_"):
|
||||||
|
continue
|
||||||
|
# 动态导入模块,触发装饰器
|
||||||
|
importlib.import_module(f".{module_name}", __package__)
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
116
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/_base.py
Normal file
116
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/_base.py
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
"""
|
||||||
|
工具注册框架
|
||||||
|
提供 ToolDef 基类和 @register_tool 装饰器,用于声明式定义 MCP 工具
|
||||||
|
"""
|
||||||
|
|
||||||
|
from abc import ABC, abstractmethod
|
||||||
|
from typing import Dict, Any, TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ..utils.api_client import AgileDBAPIClient
|
||||||
|
|
||||||
|
|
||||||
|
# 全局工具注册表
|
||||||
|
_registered_tools = []
|
||||||
|
|
||||||
|
|
||||||
|
def register_tool(name: str):
|
||||||
|
"""
|
||||||
|
工具注册装饰器
|
||||||
|
|
||||||
|
使用方式:
|
||||||
|
@register_tool("list_datasources")
|
||||||
|
class ListDatasourcesTool(ToolDef):
|
||||||
|
name = "list_datasources"
|
||||||
|
description = "获取数据源列表"
|
||||||
|
input_schema = {...}
|
||||||
|
|
||||||
|
async def execute(self, args):
|
||||||
|
return await self.client.get("/datasource/...", params=args)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
name: 工具名称(唯一标识)
|
||||||
|
"""
|
||||||
|
def decorator(cls):
|
||||||
|
# 确保类有正确的 name 属性
|
||||||
|
if not hasattr(cls, 'name') or cls.name != name:
|
||||||
|
cls.name = name
|
||||||
|
|
||||||
|
# 注册到全局列表,避免重复注册
|
||||||
|
if cls not in _registered_tools:
|
||||||
|
_registered_tools.append(cls)
|
||||||
|
return cls
|
||||||
|
|
||||||
|
return decorator
|
||||||
|
|
||||||
|
|
||||||
|
class ToolDef(ABC):
|
||||||
|
"""
|
||||||
|
工具定义基类
|
||||||
|
|
||||||
|
所有工具都应继承此类并使用 @register_tool 装饰器注册
|
||||||
|
|
||||||
|
类属性:
|
||||||
|
name: 工具名称(唯一标识)
|
||||||
|
description: 工具描述
|
||||||
|
input_schema: JSON Schema 格式的工具输入参数定义
|
||||||
|
|
||||||
|
实例属性:
|
||||||
|
client: AgileDBAPIClient 实例(由 server 注入)
|
||||||
|
"""
|
||||||
|
|
||||||
|
name: str = ""
|
||||||
|
description: str = ""
|
||||||
|
input_schema: Dict[str, Any] = {"type": "object", "properties": {}, "required": []}
|
||||||
|
|
||||||
|
def __init__(self, client: "AgileDBAPIClient"):
|
||||||
|
"""
|
||||||
|
初始化工具实例
|
||||||
|
|
||||||
|
Args:
|
||||||
|
client: API 客户端实例(由 server 注入)
|
||||||
|
"""
|
||||||
|
self.client = client
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
async def execute(self, args: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
执行工具逻辑
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: 工具输入参数(已校验)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict[str, Any]: 执行结果,将作为 MCP 工具返回值
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
def to_tool_def(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
转换为 MCP 工具定义格式
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: MCP types.Tool 所需的参数
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"name": self.name,
|
||||||
|
"description": self.description,
|
||||||
|
"inputSchema": self.input_schema,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def get_registered_tools() -> list:
|
||||||
|
"""
|
||||||
|
获取所有已注册的工具类列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
list: 所有被 @register_tool 装饰的类
|
||||||
|
"""
|
||||||
|
return list(_registered_tools)
|
||||||
|
|
||||||
|
|
||||||
|
def clear_registered_tools():
|
||||||
|
"""
|
||||||
|
清空所有已注册的工具(主要用于测试)
|
||||||
|
"""
|
||||||
|
_registered_tools.clear()
|
||||||
68
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/ai_training.py
Normal file
68
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/ai_training.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"""
|
||||||
|
AI 补全训练工具
|
||||||
|
|
||||||
|
含 generate_table_by_description 的轮询配套 get_ai_training_detail。
|
||||||
|
训练状态 trainingStatus: 0 待训练 / 1 训练中 / 2 已完成 / 3 失败
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("list_ai_trainings")
|
||||||
|
class ListAiTrainingsTool(ToolDef):
|
||||||
|
name = "list_ai_trainings"
|
||||||
|
description = "获取 AI 补全训练任务列表"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "数据源 ID(可选过滤)"},
|
||||||
|
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||||
|
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
params = {k: v for k, v in args.items() if v is not None}
|
||||||
|
return await self.client.get("/ai/training/list", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_ai_training_by_selected")
|
||||||
|
class CreateAiTrainingBySelectedTool(ToolDef):
|
||||||
|
name = "create_ai_training_by_selected"
|
||||||
|
description = "按选中的表创建 AI 补全训练任务"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||||
|
"tableIds": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "要训练的表 ID 数组",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["tableIds"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.post("/ai/training/createBySelected", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_ai_training_detail")
|
||||||
|
class GetAiTrainingDetailTool(ToolDef):
|
||||||
|
name = "get_ai_training_detail"
|
||||||
|
description = (
|
||||||
|
"查询 AI 训练任务详情(轮询用)。"
|
||||||
|
"trainingStatus: 0 待训练 / 1 训练中 / 2 已完成 / 3 失败。"
|
||||||
|
"generate_table_by_description 返回 taskId 后,用本工具轮询直到 status=2 取结果"
|
||||||
|
)
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"taskId": {"type": "string", "description": "训练任务 ID(generate_table 返回的 taskId)"},
|
||||||
|
},
|
||||||
|
"required": ["taskId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.get(f"/ai/training/{args['taskId']}")
|
||||||
124
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py
Normal file
124
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
"""
|
||||||
|
API 密钥管理工具(含创建、状态切换、删除、权限查询/授予)
|
||||||
|
|
||||||
|
注意:权限模型为「仅追加」——grant_api_key_permissions 只能新增权限,后端不支持撤销/删除
|
||||||
|
已授予的权限(真机验证 permission 删除接口返回「不支持当前的调用方式」),故不提供 revoke 工具。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("list_api_keys")
|
||||||
|
class ListApiKeysTool(ToolDef):
|
||||||
|
name = "list_api_keys"
|
||||||
|
description = "获取 API 密钥列表"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"apiKeyName": {"type": "string", "description": "密钥名称模糊搜索"},
|
||||||
|
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||||
|
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
params = {k: v for k, v in args.items() if v is not None}
|
||||||
|
return await self.client.get("/datasource/api_key/list", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_api_key")
|
||||||
|
class CreateApiKeyTool(ToolDef):
|
||||||
|
name = "create_api_key"
|
||||||
|
description = "创建新的 API 密钥"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"apiKeyName": {"type": "string", "description": "密钥名称(最多50字)"},
|
||||||
|
},
|
||||||
|
"required": ["apiKeyName"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.post("/datasource/api_key", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("toggle_api_key_status")
|
||||||
|
class ToggleApiKeyStatusTool(ToolDef):
|
||||||
|
name = "toggle_api_key_status"
|
||||||
|
description = "启用/禁用 API 密钥"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "密钥 ID"},
|
||||||
|
"status": {"type": "integer", "enum": [0, 1], "description": "0=启用, 1=禁用"},
|
||||||
|
},
|
||||||
|
"required": ["id", "status"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.put("/datasource/api_key", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("delete_api_key")
|
||||||
|
class DeleteApiKeyTool(ToolDef):
|
||||||
|
name = "delete_api_key"
|
||||||
|
description = "删除 API 密钥"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "密钥 ID"},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.delete(f"/datasource/api_key/{args['id']}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_api_key_permissions")
|
||||||
|
class GetApiKeyPermissionsTool(ToolDef):
|
||||||
|
name = "get_api_key_permissions"
|
||||||
|
description = "查看指定密钥的权限配置"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"apiKeyId": {"type": "string", "description": "密钥 ID"},
|
||||||
|
},
|
||||||
|
"required": ["apiKeyId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.get(f"/datasource/api_key/permission/{args['apiKeyId']}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("grant_api_key_permissions")
|
||||||
|
class GrantApiKeyPermissionsTool(ToolDef):
|
||||||
|
name = "grant_api_key_permissions"
|
||||||
|
description = "批量为 API 密钥授予权限(仅追加,不会覆盖或删除已有权限;后端不支持撤销已授予的权限)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"apiKeyId": {"type": "string", "description": "密钥 ID"},
|
||||||
|
"batchDatas": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "权限批量数据数组",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"connectionId": {"type": "string", "description": "数据源 ID"},
|
||||||
|
"permissionLevel": {"type": "string", "enum": ["connection", "database", "table"], "description": "权限级别"},
|
||||||
|
"permissionType": {"type": "string", "description": "权限类型(逗号分隔)"},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名(level=database/table 时)"},
|
||||||
|
"tableName": {"type": "string", "description": "表名(level=table 时)"},
|
||||||
|
},
|
||||||
|
"required": ["connectionId", "permissionLevel", "permissionType"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["apiKeyId", "batchDatas"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.post("/datasource/api_key/permission/grant_batch", json_data=args)
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
"""
|
||||||
|
库表关联配置工具(外置/内置通用)
|
||||||
|
|
||||||
|
对应文档 §2.2 / §4.2:
|
||||||
|
- postConnectionConfig 建库表关联配置(外置源建完连接后选库选表的最后一步)
|
||||||
|
- putConnectionConfig 改库表关联配置
|
||||||
|
- deleteConnectionConfig 删配置(同时也是「删除库」的实现)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_connection_config")
|
||||||
|
class CreateConnectionConfigTool(ToolDef):
|
||||||
|
name = "create_connection_config"
|
||||||
|
description = (
|
||||||
|
"创建「库表关联」配置:外置数据源建完连接后,选择要纳管的库与表的最后一步。"
|
||||||
|
"完成后数据源才能用于智能问数"
|
||||||
|
)
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"connectionId": {"type": "string", "description": "数据源(连接)ID"},
|
||||||
|
"syncTables": {"type": "boolean", "default": True, "description": "是否同步表结构,默认 true"},
|
||||||
|
"databases": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "要纳管的库及其表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"databaseName": {"type": "string", "description": "库名"},
|
||||||
|
"tableNames": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "选中的表名数组",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["databaseName", "tableNames"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["connectionId", "databases"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
args.setdefault("syncTables", True)
|
||||||
|
return await self.client.post("/datasource/config", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("update_connection_config")
|
||||||
|
class UpdateConnectionConfigTool(ToolDef):
|
||||||
|
name = "update_connection_config"
|
||||||
|
description = "更新「库表关联」配置(编辑已纳管的库表范围)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "配置 ID(编辑时必传)"},
|
||||||
|
"connectionId": {"type": "string", "description": "数据源(连接)ID"},
|
||||||
|
"syncTables": {"type": "boolean", "default": True, "description": "是否同步表结构,默认 true"},
|
||||||
|
"databases": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "要纳管的库及其表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"databaseName": {"type": "string", "description": "库名"},
|
||||||
|
"tableNames": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "选中的表名数组",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["databaseName", "tableNames"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["id", "connectionId", "databases"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
args.setdefault("syncTables", True)
|
||||||
|
return await self.client.put("/datasource/config", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("delete_connection_config")
|
||||||
|
class DeleteConnectionConfigTool(ToolDef):
|
||||||
|
name = "delete_connection_config"
|
||||||
|
description = "删除库表关联配置(批量)。这也是「删除内置库」的实现:传入对应库的配置 ID"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ids": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "配置 ID 数组",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["ids"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.post("/datasource/config/deletes", json_data={"ids": args["ids"]})
|
||||||
324
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py
Normal file
324
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py
Normal file
@@ -0,0 +1,324 @@
|
|||||||
|
"""
|
||||||
|
数据导入工具 (工具 30-31)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
from urllib.parse import urlparse, unquote
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("preview_import_data")
|
||||||
|
class PreviewImportDataTool(ToolDef):
|
||||||
|
name = "preview_import_data"
|
||||||
|
description = "从 URL 下载 Excel 文件,AI 智能识别并预览表结构/数据"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"connectionId": {"type": "string", "description": "数据源 ID"},
|
||||||
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"},
|
||||||
|
"file_url": {"type": "string", "description": "Excel 文件下载地址(.xlsx/.xls, <500KB)"},
|
||||||
|
},
|
||||||
|
"required": ["connectionId", "file_url"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
connection_id = args.pop("connectionId")
|
||||||
|
target = args.pop("target", "test")
|
||||||
|
file_url = args.pop("file_url")
|
||||||
|
|
||||||
|
# 0. 基础 URL 校验
|
||||||
|
parsed_url = urlparse(file_url)
|
||||||
|
if parsed_url.scheme not in ("http", "https"):
|
||||||
|
raise ValueError("仅支持 http/https 协议的下载地址")
|
||||||
|
|
||||||
|
# 1. 下载文件
|
||||||
|
download_resp = await self.client.client.get(file_url, follow_redirects=True, timeout=30.0)
|
||||||
|
download_resp.raise_for_status()
|
||||||
|
|
||||||
|
# 1.1 预先根据 Content-Length 拦截超大文件,避免无意义下载
|
||||||
|
max_size = 500 * 1024
|
||||||
|
content_length = download_resp.headers.get("content-length")
|
||||||
|
if content_length:
|
||||||
|
try:
|
||||||
|
if int(content_length) > max_size:
|
||||||
|
raise ValueError(
|
||||||
|
f"文件 Content-Length {content_length} bytes 超过平台限制 {max_size} bytes (500KB)"
|
||||||
|
)
|
||||||
|
except ValueError as e:
|
||||||
|
if "超过平台限制" in str(e):
|
||||||
|
raise
|
||||||
|
# 非数字时忽略
|
||||||
|
|
||||||
|
file_content = download_resp.content
|
||||||
|
|
||||||
|
# 2. 文件大小校验(平台限制 500KB)
|
||||||
|
if len(file_content) > max_size:
|
||||||
|
raise ValueError(
|
||||||
|
f"文件大小 {len(file_content)} bytes 超过平台限制 {max_size} bytes (500KB),"
|
||||||
|
"请先压缩或拆分后重试"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 3. 解析文件名并校验扩展名
|
||||||
|
file_name = self._extract_file_name(download_resp, file_url)
|
||||||
|
allowed_exts = (".xlsx", ".xls")
|
||||||
|
if not file_name.lower().endswith(allowed_exts):
|
||||||
|
raise ValueError(
|
||||||
|
f"不支持的文件类型: {file_name},仅支持 {', '.join(allowed_exts)}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. 推断 MIME 类型
|
||||||
|
content_type, _ = mimetypes.guess_type(file_name)
|
||||||
|
if not content_type:
|
||||||
|
content_type = (
|
||||||
|
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||||
|
if file_name.lower().endswith(".xlsx")
|
||||||
|
else "application/vnd.ms-excel"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. 构建文件上传
|
||||||
|
files = {
|
||||||
|
"file": (file_name, io.BytesIO(file_content), content_type),
|
||||||
|
}
|
||||||
|
|
||||||
|
return await self.client.upload(
|
||||||
|
f"/datasource/connection/{connection_id}/import_document/preview",
|
||||||
|
files=files,
|
||||||
|
params={"target": target},
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _extract_file_name(response, file_url: str) -> str:
|
||||||
|
"""从响应头或 URL 中提取文件名。"""
|
||||||
|
# 优先从 Content-Disposition 解析
|
||||||
|
content_disposition = response.headers.get("content-disposition")
|
||||||
|
if content_disposition:
|
||||||
|
# 简单解析 filename="xxx" 或 filename*=UTF-8''xxx
|
||||||
|
# 注意:filename* 也包含 filename= 前缀,必须先检查 filename*
|
||||||
|
for part in content_disposition.split(";"):
|
||||||
|
part = part.strip()
|
||||||
|
if part.lower().startswith("filename*="):
|
||||||
|
encoded = part.split("=", 1)[1]
|
||||||
|
# 形如 UTF-8''xxx
|
||||||
|
if "''" in encoded:
|
||||||
|
_, _, file_name = encoded.partition("''")
|
||||||
|
return os.path.basename(unquote(file_name, encoding="utf-8"))
|
||||||
|
return os.path.basename(unquote(encoded, encoding="utf-8"))
|
||||||
|
if part.lower().startswith("filename="):
|
||||||
|
file_name = part.split("=", 1)[1].strip('"')
|
||||||
|
return os.path.basename(unquote(file_name))
|
||||||
|
|
||||||
|
# fallback 从 URL 路径获取
|
||||||
|
parsed = urlparse(file_url)
|
||||||
|
path = unquote(parsed.path)
|
||||||
|
file_name = os.path.basename(path) if path else ""
|
||||||
|
if file_name and "." in file_name:
|
||||||
|
return file_name
|
||||||
|
|
||||||
|
return "import.xlsx"
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("confirm_import_data")
|
||||||
|
class ConfirmImportDataTool(ToolDef):
|
||||||
|
name = "confirm_import_data"
|
||||||
|
description = (
|
||||||
|
"确认导入 AI 识别后的数据(建表+插数据),第二步。第一步先调 preview_import_data。\n"
|
||||||
|
"【data 传什么】把 preview_import_data 返回的 data 原文整块传给 data 参数即可,"
|
||||||
|
"工具会自动解包并组装成后端要求的 {tableStructure(单表对象,含databaseName), allData} 结构。\n"
|
||||||
|
"data 的标准形态(= preview 的返回):\n"
|
||||||
|
" {\n"
|
||||||
|
" \"tableStructure\": { \"success\":true, \"message\":\"...\",\n"
|
||||||
|
" \"data\": { \"tables\": [ { \"tableName\":\"animals\", \"columns\":[...] } ] },\n"
|
||||||
|
" \"allData\": [ [列名表头行...], [行1各列值...], [行2各列值...] ] },\n"
|
||||||
|
" \"databaseName\": \"目标库名\", \"target\": \"prod|test\"\n"
|
||||||
|
" }\n"
|
||||||
|
"【allData 的结构(关键)】allData 是二维数组:\n"
|
||||||
|
" · 首行 allData[0] 是【表头行】= 各列的 columnName(列名),真实数据从 allData[1] 起;\n"
|
||||||
|
" · 每行(含表头行)都是「按 columns 顺序排列的位置数组」,行宽 = 列总数(含 SERIAL 主键等所有列,不裁剪);\n"
|
||||||
|
" · 若调用方传的 allData 没带表头行(首行就是数据),工具会据列名自动补一行表头——"
|
||||||
|
"否则后端会把首行数据当成字段名,报「查询字段不存在/字段名称不正确」。\n"
|
||||||
|
"【列名必须对应目标表真实字段】tableStructure.columns(及 allData 首行表头)里的列名,"
|
||||||
|
"必须是目标表中确实存在的字段名(后端按列名拼 INSERT)。导入到已有表时,"
|
||||||
|
"不要直接用 Excel 识别出的列,应先调 get_table_detail 拿到目标表真实字段,"
|
||||||
|
"再把 columns、表头、各行取值对齐到这些真实字段,否则报「查询字段不存在/字段名称不正确」。\n"
|
||||||
|
"databaseName/target 既可放顶层参数,也可放在 data 里,工具都能识别。"
|
||||||
|
)
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"connectionId": {"type": "string", "description": "数据源连接 ID(同 preview 用的那个)"},
|
||||||
|
"databaseName": {"type": "string", "description": "落库的数据库名(导入目标库)。必填——顶层不给会尝试从 data.databaseName 回捞"},
|
||||||
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test。可放顶层或 data 里"},
|
||||||
|
"data": {
|
||||||
|
"type": "object",
|
||||||
|
"description": (
|
||||||
|
"preview_import_data 返回的 data 原文整块(含 tableStructure{success,message,data:{tables:[...]}} 与 allData)。"
|
||||||
|
"allData 为二维数组:首行是列名表头(allData[0])、数据行从 allData[1] 起,"
|
||||||
|
"每行按 columns 顺序给出全部列的值,行宽 = 列总数(不裁剪自增列)。"
|
||||||
|
"缺表头时工具会据列名自动补。也接受调用方已组装好的最终结构。"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["connectionId", "databaseName", "data"],
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_body(data: dict, database_name) -> dict:
|
||||||
|
"""把 preview 返回的 data 组装成 confirm 要求的 body。
|
||||||
|
|
||||||
|
后端真实结构(已真机探测确认):
|
||||||
|
{ "tableStructure": <单表对象,含 columns 且需带 databaseName>, "allData": [...] }
|
||||||
|
preview 返回的 data.tableStructure 是 {success,message,data:{tables:[...]}} 的包装,
|
||||||
|
需取出 data.tables[0] 作为单表对象。
|
||||||
|
"""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return data
|
||||||
|
|
||||||
|
# 若传入的是 preview 工具的完整返回信封 {code, msg, data:{...}},先剥一层
|
||||||
|
if "tableStructure" not in data and isinstance(data.get("data"), dict):
|
||||||
|
inner_env = data["data"]
|
||||||
|
if "tableStructure" in inner_env or "allData" in inner_env:
|
||||||
|
data = inner_env
|
||||||
|
|
||||||
|
ts = data.get("tableStructure")
|
||||||
|
single_table = None
|
||||||
|
ts_inner = {}
|
||||||
|
if isinstance(ts, dict):
|
||||||
|
if "columns" in ts:
|
||||||
|
# 已是单表对象(调用方自行组装过)
|
||||||
|
single_table = dict(ts)
|
||||||
|
else:
|
||||||
|
# preview 包装:tableStructure.data.tables[0]
|
||||||
|
ts_inner = ts.get("data") if isinstance(ts.get("data"), dict) else {}
|
||||||
|
tables = ts_inner.get("tables") if isinstance(ts_inner, dict) else None
|
||||||
|
if isinstance(tables, list) and tables:
|
||||||
|
single_table = dict(tables[0])
|
||||||
|
|
||||||
|
if single_table is None:
|
||||||
|
# 无法识别结构,原样透传(兼容调用方已构造好完整 body 的情况)
|
||||||
|
return data
|
||||||
|
|
||||||
|
if database_name and not single_table.get("databaseName"):
|
||||||
|
single_table["databaseName"] = database_name
|
||||||
|
|
||||||
|
# allData 可能落在多个层级(取决于调用方/preview 的嵌套方式),按优先级查找:
|
||||||
|
# 1. data.allData —— 与 tableStructure 平级(约定的标准位置)
|
||||||
|
# 2. tableStructure.allData —— 嵌在 tableStructure 包装内(真机/AI 常见误放)
|
||||||
|
# 3. tableStructure.data.allData —— 嵌在内层 data 里
|
||||||
|
# 注意:只接受 list;任何非 list(如内层 data 的 tables 包装对象)都视为未命中,
|
||||||
|
# 避免把 dict 误当成行数据传给后端。
|
||||||
|
all_data = None
|
||||||
|
for candidate in (
|
||||||
|
data.get("allData"),
|
||||||
|
ts.get("allData") if isinstance(ts, dict) else None,
|
||||||
|
ts_inner.get("allData") if isinstance(ts_inner, dict) else None,
|
||||||
|
):
|
||||||
|
if isinstance(candidate, list):
|
||||||
|
all_data = candidate
|
||||||
|
break
|
||||||
|
if all_data is None:
|
||||||
|
all_data = []
|
||||||
|
|
||||||
|
# 表头行:后端约定 allData[0] 是「表头行」(列名数组),真实数据从 allData[1] 起
|
||||||
|
# (见前端 TableRecognition.vue handleComplete 与 CustomizeDbTable.vue validateDataColumns)。
|
||||||
|
# 若调用方传的 allData 没带表头(首行就是数据),后端会把首行数据当成字段名,
|
||||||
|
# 报「查询字段不存在/字段名称不正确」。这里据列名补出表头行。
|
||||||
|
all_data = ConfirmImportDataTool._ensure_header(single_table.get("columns"), all_data)
|
||||||
|
|
||||||
|
return {"tableStructure": single_table, "allData": all_data}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _column_names(columns):
|
||||||
|
"""从列定义中按顺序提取列名数组。"""
|
||||||
|
if not isinstance(columns, list):
|
||||||
|
return []
|
||||||
|
return [c.get("columnName") for c in columns if isinstance(c, dict) and c.get("columnName")]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _ensure_header(columns, all_data):
|
||||||
|
"""确保 allData[0] 是「表头行」(列名数组)。
|
||||||
|
|
||||||
|
后端约定:allData[0] 为表头(列名),真实数据行从 allData[1] 起;数据行按 columns
|
||||||
|
顺序给出【全部列】的值(不裁剪自增列)。前端 TableRecognition.vue 在提交前总会把
|
||||||
|
列名作为首行 push 进 allData。若调用方(含 AI)传来的 allData 首行已经是数据(缺表头),
|
||||||
|
后端会把首行当列名解析,报「查询字段不存在/字段名称不正确」。这里据列名补表头:
|
||||||
|
- 首行恰好等于列名数组 → 视为已带表头,原样返回
|
||||||
|
- 否则 → 在最前面补一行列名
|
||||||
|
"""
|
||||||
|
names = ConfirmImportDataTool._column_names(columns)
|
||||||
|
if not names or not isinstance(all_data, list) or not all_data:
|
||||||
|
return all_data
|
||||||
|
first = all_data[0]
|
||||||
|
if isinstance(first, list) and list(first) == names:
|
||||||
|
return all_data # 已带表头
|
||||||
|
return [names, *all_data]
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
connection_id = args.pop("connectionId")
|
||||||
|
target = args.pop("target", None)
|
||||||
|
database_name = args.pop("databaseName", None)
|
||||||
|
data = args.pop("data")
|
||||||
|
|
||||||
|
# 容错:databaseName / target 可能被放进 data 里(AI 常把 preview 返回的整块连同
|
||||||
|
# databaseName/target 一起塞进 data)。顶层没给时,从 data 里回捞,并清出 data
|
||||||
|
# 避免污染最终 body。
|
||||||
|
if isinstance(data, dict):
|
||||||
|
if database_name is None and data.get("databaseName"):
|
||||||
|
database_name = data.get("databaseName")
|
||||||
|
if target is None and data.get("target"):
|
||||||
|
target = data.get("target")
|
||||||
|
data = {k: v for k, v in data.items() if k not in ("databaseName", "target")}
|
||||||
|
|
||||||
|
if target is None:
|
||||||
|
target = "test"
|
||||||
|
|
||||||
|
body = self._build_body(data, database_name)
|
||||||
|
|
||||||
|
# 预检:把后端那两个含糊的报错(「导入数据不能为空」/「数据库名称不能为空」)
|
||||||
|
# 提前在工具层拦下,给出可操作的提示(指明 allData/databaseName 该放哪),
|
||||||
|
# 避免调用方对着后端原文反复试错。仅在 body 已被识别为标准结构时校验。
|
||||||
|
if isinstance(body, dict) and "tableStructure" in body:
|
||||||
|
if not body.get("allData"):
|
||||||
|
raise ValueError(
|
||||||
|
"导入数据为空:未能从 data 中解析到 allData(数据行)。"
|
||||||
|
"请确认 allData 是一个非空数组,可放在 data.allData、"
|
||||||
|
"data.tableStructure.allData 或 data.tableStructure.data.allData 任一层级。"
|
||||||
|
)
|
||||||
|
ts = body["tableStructure"]
|
||||||
|
if isinstance(ts, dict) and not ts.get("databaseName"):
|
||||||
|
raise ValueError(
|
||||||
|
"缺少 databaseName(落库目标库名):请通过顶层参数 databaseName 传入,"
|
||||||
|
"或放在 data.databaseName 中(工具会自动塞进表对象)。"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 行宽与表头校验:_ensure_header 已保证 allData[0] 是表头行(列名)。
|
||||||
|
# 后端要求每行(含表头)宽度 = 列数(全部列,含自增列占位),且表头之外至少有 1 行数据。
|
||||||
|
# 行宽对不上后端只会回含糊的「字段名称不正确/查询字段不存在」,这里提前报清楚。
|
||||||
|
cols = ts.get("columns") if isinstance(ts, dict) else None
|
||||||
|
all_data = body["allData"]
|
||||||
|
if isinstance(cols, list) and cols:
|
||||||
|
total = len(cols)
|
||||||
|
# 表头之外至少要有一行真实数据
|
||||||
|
data_rows = [r for r in all_data[1:] if isinstance(r, list)]
|
||||||
|
if not data_rows:
|
||||||
|
raise ValueError(
|
||||||
|
"导入数据为空:allData 除表头行外没有任何数据行。"
|
||||||
|
"allData 约定首行为表头(列名),真实数据从第 2 行起。"
|
||||||
|
)
|
||||||
|
for idx, row in enumerate(all_data):
|
||||||
|
if isinstance(row, list) and len(row) != total:
|
||||||
|
raise ValueError(
|
||||||
|
f"第 {idx + 1} 行列数为 {len(row)},与表结构的 {total} 列不匹配。"
|
||||||
|
"allData 每行(含表头行)都应按 columns 顺序给出全部列的值;"
|
||||||
|
"首行须为列名表头,数据行从第 2 行起。请核对是否多/少了列。"
|
||||||
|
)
|
||||||
|
|
||||||
|
return await self.client.post(
|
||||||
|
f"/datasource/connection/{connection_id}/import_document/confirm",
|
||||||
|
json_data=body,
|
||||||
|
params={"target": target},
|
||||||
|
)
|
||||||
286
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py
Normal file
286
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py
Normal file
@@ -0,0 +1,286 @@
|
|||||||
|
"""
|
||||||
|
数据库与表管理工具 (工具 7-12)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("list_databases")
|
||||||
|
class ListDatabasesTool(ToolDef):
|
||||||
|
name = "list_databases"
|
||||||
|
description = "获取指定数据源下的数据库列表"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||||
|
"sourceType": {"type": "string", "enum": ["builtin", "external"], "description": "数据源类型"},
|
||||||
|
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||||
|
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
params = {k: v for k, v in args.items() if v is not None}
|
||||||
|
return await self.client.get("/datasource/config/list", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("list_tables")
|
||||||
|
class ListTablesTool(ToolDef):
|
||||||
|
name = "list_tables"
|
||||||
|
description = "获取指定数据源下的表列表"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||||
|
"sourceType": {"type": "string", "enum": ["builtin", "external"], "description": "数据源类型"},
|
||||||
|
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||||
|
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
params = {k: v for k, v in args.items() if v is not None}
|
||||||
|
return await self.client.get("/datasource/table/list", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("list_tables_with_ai")
|
||||||
|
class ListTablesWithAiTool(ToolDef):
|
||||||
|
name = "list_tables_with_ai"
|
||||||
|
description = (
|
||||||
|
"获取表元数据列表(含 AI 训练描述),内置库管理的主列表。"
|
||||||
|
"注意:datasourceId 实为「配置/库」ID(即 create_connection_config 落库后的 config id)"
|
||||||
|
)
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "配置/库 ID(非数据源连接 ID)"},
|
||||||
|
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||||
|
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
params = {k: v for k, v in args.items() if v is not None}
|
||||||
|
return await self.client.get("/datasource/table/ailist", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_table_detail")
|
||||||
|
class GetTableDetailTool(ToolDef):
|
||||||
|
name = "get_table_detail"
|
||||||
|
description = "获取表的完整结构信息(字段列表、主键、类型等)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
|
},
|
||||||
|
"required": ["tableId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
table_id = args["tableId"]
|
||||||
|
return await self.client.get(f"/datasource/table/{table_id}/detail")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_table")
|
||||||
|
class CreateTableTool(ToolDef):
|
||||||
|
name = "create_table"
|
||||||
|
description = "在指定数据库创建新表"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "目标数据库名"},
|
||||||
|
"tableName": {"type": "string", "description": "表名(小写字母+数字+下划线)"},
|
||||||
|
"tableComment": {"type": "string", "description": "表注释"},
|
||||||
|
"columns": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "字段定义数组",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"columnName": {"type": "string", "description": "字段名"},
|
||||||
|
"columnType": {"type": "string", "description": "字段类型(VARCHAR/INTEGER/SERIAL等)"},
|
||||||
|
"columnLength": {"type": "integer", "description": "字段长度"},
|
||||||
|
"isPrimaryKey": {"type": "boolean", "description": "是否主键"},
|
||||||
|
"isNullable": {"type": "boolean", "description": "是否可空"},
|
||||||
|
"isAutoIncrement": {"type": "boolean", "description": "是否自增"},
|
||||||
|
"columnComment": {"type": "string", "description": "字段注释"},
|
||||||
|
"defaultValue": {"type": "string", "description": "默认值"},
|
||||||
|
},
|
||||||
|
"required": ["columnName", "columnType"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["connectionId", "databaseName", "tableName", "columns"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
connection_id = args.pop("connectionId")
|
||||||
|
return await self.client.post(f"/datasource/connection/{connection_id}/create_table", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("alter_table")
|
||||||
|
class AlterTableTool(ToolDef):
|
||||||
|
name = "alter_table"
|
||||||
|
description = "修改已有表结构(增/改/删字段)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名"},
|
||||||
|
"tableName": {"type": "string", "description": "表名"},
|
||||||
|
"operations": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "表结构变更操作数组",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"operation": {"type": "string", "enum": ["ADD_COLUMN", "MODIFY_COLUMN", "DROP_COLUMN"], "description": "变更类型:新增/修改/删除字段(后端实测仅支持这三种)"},
|
||||||
|
"column": {"type": "object", "description": "列定义。ADD/MODIFY 需 columnName+columnType(+columnLength/columnComment 等);DROP 仅需 columnName"},
|
||||||
|
},
|
||||||
|
"required": ["operation", "column"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tableComment": {"type": "string", "description": "表注释"},
|
||||||
|
},
|
||||||
|
"required": ["connectionId", "databaseName", "tableName", "operations"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
connection_id = args.pop("connectionId")
|
||||||
|
return await self.client.put(f"/datasource/connection/{connection_id}/alter_table", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("generate_table_by_description")
|
||||||
|
class GenerateTableByDescriptionTool(ToolDef):
|
||||||
|
name = "generate_table_by_description"
|
||||||
|
description = "通过自然语言描述让 AI 生成表结构(异步任务,返回 taskId)。需配合 get_ai_training_detail 轮询 taskId 获取生成结果"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"requirement": {"type": "string", "description": "业务需求描述"},
|
||||||
|
"databaseId": {"type": "integer", "description": "关联的数据库 ID(可选)"},
|
||||||
|
},
|
||||||
|
"required": ["requirement"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.post("/datasource/connection/generate_table", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_database")
|
||||||
|
class CreateDatabaseTool(ToolDef):
|
||||||
|
name = "create_database"
|
||||||
|
description = "在内置数据源下创建一个数据库(建表前置步骤)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名(仅字母数字下划线)"},
|
||||||
|
},
|
||||||
|
"required": ["connectionId", "databaseName"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
connection_id = args.pop("connectionId")
|
||||||
|
return await self.client.post(f"/datasource/connection/{connection_id}/create_database", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_database_table")
|
||||||
|
class CreateDatabaseTableTool(ToolDef):
|
||||||
|
name = "create_database_table"
|
||||||
|
description = (
|
||||||
|
"一次性创建数据库 + 表(合并接口,免去先建库再建表)。"
|
||||||
|
"支持两种用法:①单表便捷参数 tableName/tableComment/columns;"
|
||||||
|
"②批量传 tables 数组(一次建多张表)。二者任选其一"
|
||||||
|
)
|
||||||
|
# 单字段(columnName/columnType/...)的 schema,单表和批量两种用法共用
|
||||||
|
_COLUMN_ITEMS = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"columnName": {"type": "string", "description": "字段名"},
|
||||||
|
"columnType": {"type": "string", "description": "字段类型(VARCHAR/INTEGER/SERIAL等)"},
|
||||||
|
"columnLength": {"type": "integer", "description": "字段长度"},
|
||||||
|
"isPrimaryKey": {"type": "boolean", "description": "是否主键"},
|
||||||
|
"isNullable": {"type": "boolean", "description": "是否可空"},
|
||||||
|
"isAutoIncrement": {"type": "boolean", "description": "是否自增"},
|
||||||
|
"columnComment": {"type": "string", "description": "字段注释"},
|
||||||
|
"defaultValue": {"type": "string", "description": "默认值"},
|
||||||
|
},
|
||||||
|
"required": ["columnName", "columnType"],
|
||||||
|
}
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名(仅字母数字下划线)"},
|
||||||
|
# —— 用法①:单表便捷参数 ——
|
||||||
|
"tableName": {"type": "string", "description": "表名(用法①单表,小写字母+数字+下划线)"},
|
||||||
|
"tableComment": {"type": "string", "description": "表注释(用法①单表)"},
|
||||||
|
"columns": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "字段定义数组(用法①单表,结构同 create_table)",
|
||||||
|
"items": _COLUMN_ITEMS,
|
||||||
|
},
|
||||||
|
# —— 用法②:批量 tables 数组 ——
|
||||||
|
"tables": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "表定义数组(用法②批量建多表),每项含 tableName/tableComment/columns",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tableName": {"type": "string", "description": "表名"},
|
||||||
|
"tableComment": {"type": "string", "description": "表注释"},
|
||||||
|
"columns": {"type": "array", "description": "字段定义数组", "items": _COLUMN_ITEMS},
|
||||||
|
},
|
||||||
|
"required": ["tableName", "columns"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["connectionId", "databaseName"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
connection_id = args.pop("connectionId")
|
||||||
|
# 后端真实 body 结构:{databaseName, tables:[{tableName,tableComment,columns}]}(已真机探测确认)
|
||||||
|
if not args.get("tables"):
|
||||||
|
table = {"tableName": args.pop("tableName", None), "columns": args.pop("columns", [])}
|
||||||
|
if args.get("tableComment") is not None:
|
||||||
|
table["tableComment"] = args.pop("tableComment")
|
||||||
|
args["tables"] = [table]
|
||||||
|
else:
|
||||||
|
# 批量用法下,清掉可能并存的单表字段,避免歧义
|
||||||
|
args.pop("tableName", None)
|
||||||
|
args.pop("tableComment", None)
|
||||||
|
args.pop("columns", None)
|
||||||
|
return await self.client.post(f"/datasource/connection/{connection_id}/create_database_table", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("alter_database")
|
||||||
|
class AlterDatabaseTool(ToolDef):
|
||||||
|
name = "alter_database"
|
||||||
|
description = "修改数据库(如改名)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "当前数据库名"},
|
||||||
|
"newName": {"type": "string", "description": "新数据库名"},
|
||||||
|
},
|
||||||
|
"required": ["connectionId", "databaseName", "newName"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
connection_id = args.pop("connectionId")
|
||||||
|
# 后端改名字段名为 newName(已真机探测确认);兼容历史传参 newDatabaseName
|
||||||
|
if "newName" not in args and "newDatabaseName" in args:
|
||||||
|
args["newName"] = args.pop("newDatabaseName")
|
||||||
|
return await self.client.put(f"/datasource/connection/{connection_id}/alter_database", json_data=args)
|
||||||
249
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py
Normal file
249
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
"""
|
||||||
|
数据源管理工具 (工具 1-6)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("list_datasources")
|
||||||
|
class ListDatasourcesTool(ToolDef):
|
||||||
|
name = "list_datasources"
|
||||||
|
description = "获取数据源列表,支持搜索和状态筛选"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceName": {"type": "string", "description": "数据源名称模糊搜索"},
|
||||||
|
"status": {"type": "integer", "description": "0=运行中, 1=已停止, 不传=全部"},
|
||||||
|
"sourceType": {"type": "string", "enum": ["builtin", "external"], "description": "数据源类型"},
|
||||||
|
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||||
|
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
params = {k: v for k, v in args.items() if v is not None}
|
||||||
|
return await self.client.get("/datasource/connection/list", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_datasource_detail")
|
||||||
|
class GetDatasourceDetailTool(ToolDef):
|
||||||
|
name = "get_datasource_detail"
|
||||||
|
description = "获取单个数据源的完整详情(含配置和实时结构)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
ds_id = args["datasourceId"]
|
||||||
|
result = {}
|
||||||
|
# 获取基本信息
|
||||||
|
try:
|
||||||
|
result["detail"] = await self.client.get(f"/datasource/connection/{ds_id}")
|
||||||
|
except Exception as e:
|
||||||
|
result["detail"] = {"error": str(e)}
|
||||||
|
# 获取配置
|
||||||
|
try:
|
||||||
|
result["config"] = await self.client.get(f"/datasource/config/{ds_id}")
|
||||||
|
except Exception as e:
|
||||||
|
result["config"] = {"error": str(e)}
|
||||||
|
# 获取实时结构
|
||||||
|
try:
|
||||||
|
result["structure"] = await self.client.get(f"/datasource/connection/realtime/structure/{ds_id}")
|
||||||
|
except Exception as e:
|
||||||
|
result["structure"] = {"error": str(e)}
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_datasource")
|
||||||
|
class CreateDatasourceTool(ToolDef):
|
||||||
|
name = "create_datasource"
|
||||||
|
description = "创建外部数据源连接(可选先测试连接)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceName": {"type": "string", "description": "数据源名称(3-20字)"},
|
||||||
|
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
|
||||||
|
"host": {"type": "string", "description": "数据库地址"},
|
||||||
|
"port": {"type": "integer", "description": "端口号"},
|
||||||
|
"databaseName": {"type": "string", "description": "要连接的数据库名"},
|
||||||
|
"username": {"type": "string", "description": "数据库用户名"},
|
||||||
|
"password": {"type": "string", "description": "密码"},
|
||||||
|
"remark": {"type": "string", "description": "数据源描述"},
|
||||||
|
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型,默认 user_password"},
|
||||||
|
"test_first": {"type": "boolean", "default": True, "description": "是否先测试连接,默认 true"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceName", "datasourceType", "host", "port", "databaseName", "username"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
test_first = args.pop("test_first", True)
|
||||||
|
|
||||||
|
# 如果需要先测试连接(测试失败时 client 会直接抛异常,由上层统一捕获)
|
||||||
|
if test_first:
|
||||||
|
test_data = {
|
||||||
|
"datasourceName": args.get("datasourceName"),
|
||||||
|
"datasourceType": args.get("datasourceType"),
|
||||||
|
"host": args.get("host"),
|
||||||
|
"port": args.get("port"),
|
||||||
|
"databaseName": args.get("databaseName"),
|
||||||
|
"username": args.get("username"),
|
||||||
|
"password": args.get("password"),
|
||||||
|
"connectionType": args.get("connectionType", "user_password"),
|
||||||
|
}
|
||||||
|
await self.client.post("/datasource/connection/test", json_data=test_data)
|
||||||
|
|
||||||
|
# 创建数据源
|
||||||
|
return await self.client.post("/datasource/connection", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("update_datasource")
|
||||||
|
class UpdateDatasourceTool(ToolDef):
|
||||||
|
name = "update_datasource"
|
||||||
|
description = "更新数据源连接信息"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "数据源 ID"},
|
||||||
|
"datasourceName": {"type": "string", "description": "更新名称"},
|
||||||
|
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
|
||||||
|
"host": {"type": "string", "description": "更新地址"},
|
||||||
|
"port": {"type": "integer", "description": "更新端口"},
|
||||||
|
"databaseName": {"type": "string", "description": "更新数据库名"},
|
||||||
|
"username": {"type": "string", "description": "更新用户名"},
|
||||||
|
"password": {"type": "string", "description": "新密码(不传则不变)"},
|
||||||
|
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型"},
|
||||||
|
"remark": {"type": "string", "description": "更新描述"},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.put("/datasource/connection", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("toggle_datasource_status")
|
||||||
|
class ToggleDatasourceStatusTool(ToolDef):
|
||||||
|
name = "toggle_datasource_status"
|
||||||
|
description = "启用/停用数据源"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "数据源 ID"},
|
||||||
|
"status": {"type": "integer", "enum": [0, 1], "description": "0=启用, 1=停用"},
|
||||||
|
},
|
||||||
|
"required": ["id", "status"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.put("/datasource/connection/changeStatus", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("delete_datasource")
|
||||||
|
class DeleteDatasourceTool(ToolDef):
|
||||||
|
name = "delete_datasource"
|
||||||
|
description = "删除数据源(运行中会自动先停用)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "数据源 ID"},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
ds_id = args["id"]
|
||||||
|
# 先尝试停用(仅忽略已停用等预期错误)
|
||||||
|
try:
|
||||||
|
await self.client.put("/datasource/connection/changeStatus", json_data={"id": ds_id, "status": 1})
|
||||||
|
except Exception as e:
|
||||||
|
# 记录日志但继续删除
|
||||||
|
logger.debug(f"停用数据源失败(可能已停用): {e}")
|
||||||
|
|
||||||
|
# 删除数据源
|
||||||
|
return await self.client.delete(f"/datasource/connection/{ds_id}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("test_connection")
|
||||||
|
class TestConnectionTool(ToolDef):
|
||||||
|
name = "test_connection"
|
||||||
|
description = "测试外部数据库连接是否可用(不创建数据源)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
|
||||||
|
"host": {"type": "string", "description": "数据库地址"},
|
||||||
|
"port": {"type": "integer", "description": "端口号"},
|
||||||
|
"databaseName": {"type": "string", "description": "要连接的数据库名"},
|
||||||
|
"username": {"type": "string", "description": "数据库用户名"},
|
||||||
|
"password": {"type": "string", "description": "密码"},
|
||||||
|
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型,默认 user_password"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceType", "host", "port", "databaseName", "username"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
args.setdefault("connectionType", "user_password")
|
||||||
|
return await self.client.post("/datasource/connection/test", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_realtime_structure")
|
||||||
|
class GetRealtimeStructureTool(ToolDef):
|
||||||
|
name = "get_realtime_structure"
|
||||||
|
description = "拉取数据源真实的库表结构(实时探测远端数据库)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
ds_id = args["datasourceId"]
|
||||||
|
return await self.client.get(f"/datasource/connection/realtime/structure/{ds_id}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_builtin_datasource")
|
||||||
|
class CreateBuiltinDatasourceTool(ToolDef):
|
||||||
|
name = "create_builtin_datasource"
|
||||||
|
description = "创建内置 PostgreSQL 数据源连接,返回 connectionId(内置源建库建表链路第一步)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceName": {"type": "string", "description": "数据源名称(3-20字)"},
|
||||||
|
"remark": {"type": "string", "description": "数据源描述"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceName"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.post("/datasource/connection/create_builtin_postgresql", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("update_builtin_datasource")
|
||||||
|
class UpdateBuiltinDatasourceTool(ToolDef):
|
||||||
|
name = "update_builtin_datasource"
|
||||||
|
description = "修改内置 PostgreSQL 数据源连接信息"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "数据源 ID"},
|
||||||
|
"datasourceName": {"type": "string", "description": "更新名称"},
|
||||||
|
"remark": {"type": "string", "description": "更新描述"},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.put("/datasource/connection/update_builtin_database", json_data=args)
|
||||||
151
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/mqtt_sync.py
Normal file
151
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/mqtt_sync.py
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
"""
|
||||||
|
MQTT 字段关联同步工具(内置源专属,文档 §8)
|
||||||
|
|
||||||
|
接口前缀为 /system/mqtt(注意与 /datasource 不同)。
|
||||||
|
|
||||||
|
关于 sourceTable / targetTable 的方向(已真机验证):
|
||||||
|
直接调后端 API 时按字面透传即可——create 传什么,detail 回显就是什么,
|
||||||
|
字段自洽、后端不做调换。例:create 传 sourceTable=sys_user/targetTable=users,
|
||||||
|
detail 读回仍是 sourceTable=sys_user/targetTable=users。
|
||||||
|
文档 §8 提到的「前端提交时调换」是前端 Vue 组件为 UI 展示所做的转换,
|
||||||
|
与后端 API 契约无关,本工具(直连 API)无需调换。
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("list_mqtt_configs")
|
||||||
|
class ListMqttConfigsTool(ToolDef):
|
||||||
|
name = "list_mqtt_configs"
|
||||||
|
description = "获取 MQTT 字段关联同步配置列表"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||||
|
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
params = {k: v for k, v in args.items() if v is not None}
|
||||||
|
return await self.client.get("/system/mqtt/config/list", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_mqtt_config_detail")
|
||||||
|
class GetMqttConfigDetailTool(ToolDef):
|
||||||
|
name = "get_mqtt_config_detail"
|
||||||
|
description = "获取 MQTT 同步配置详情"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"configId": {"type": "string", "description": "配置 ID"},
|
||||||
|
},
|
||||||
|
"required": ["configId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.get(f"/system/mqtt/config/{args['configId']}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("list_mqtt_target_tables")
|
||||||
|
class ListMqttTargetTablesTool(ToolDef):
|
||||||
|
name = "list_mqtt_target_tables"
|
||||||
|
description = "获取 MQTT 可同步的目标表列表"
|
||||||
|
input_schema = {"type": "object", "properties": {}, "required": []}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.get("/system/mqtt/config/tableList")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("list_mqtt_target_table_columns")
|
||||||
|
class ListMqttTargetTableColumnsTool(ToolDef):
|
||||||
|
name = "list_mqtt_target_table_columns"
|
||||||
|
description = "获取 MQTT 目标表的字段列表"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tableName": {"type": "string", "description": "目标表名"},
|
||||||
|
},
|
||||||
|
"required": ["tableName"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.get(f"/system/mqtt/config/tableColumns/{args['tableName']}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_mqtt_config")
|
||||||
|
class CreateMqttConfigTool(ToolDef):
|
||||||
|
name = "create_mqtt_config"
|
||||||
|
description = (
|
||||||
|
"新增 MQTT 字段关联同步配置。"
|
||||||
|
"sourceTable/targetTable 按字面透传即可(实测确认 create 存入与 detail 回显一致,"
|
||||||
|
"无需调换;文档§8 的调换是前端 UI 层行为,与本 API 无关)"
|
||||||
|
)
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"sourceTable": {"type": "string", "description": "源表名"},
|
||||||
|
"targetTable": {"type": "string", "description": "目标表名"},
|
||||||
|
"config": {"type": "object", "description": "完整配置体(字段映射等),按后端结构透传"},
|
||||||
|
},
|
||||||
|
"required": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
# config 若存在则以其为主体,否则直接用 args(去掉 config 包裹键)
|
||||||
|
body = args.get("config") if isinstance(args.get("config"), dict) else dict(args)
|
||||||
|
return await self.client.post("/system/mqtt/config", json_data=body)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("update_mqtt_config")
|
||||||
|
class UpdateMqttConfigTool(ToolDef):
|
||||||
|
name = "update_mqtt_config"
|
||||||
|
description = (
|
||||||
|
"修改 MQTT 字段关联同步配置。"
|
||||||
|
"sourceTable/targetTable 按字面透传即可(实测确认无需调换,详见 create_mqtt_config)"
|
||||||
|
)
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "配置 ID"},
|
||||||
|
"sourceTable": {"type": "string", "description": "源表名"},
|
||||||
|
"targetTable": {"type": "string", "description": "目标表名"},
|
||||||
|
"config": {"type": "object", "description": "完整配置体,按后端结构透传"},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
if isinstance(args.get("config"), dict):
|
||||||
|
body = dict(args["config"])
|
||||||
|
body.setdefault("id", args.get("id"))
|
||||||
|
else:
|
||||||
|
body = dict(args)
|
||||||
|
return await self.client.put("/system/mqtt/config", json_data=body)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("delete_mqtt_config")
|
||||||
|
class DeleteMqttConfigTool(ToolDef):
|
||||||
|
name = "delete_mqtt_config"
|
||||||
|
description = "删除 MQTT 同步配置(支持多个 id,逗号拼接)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"ids": {"type": "string", "description": "配置 ID,多个用逗号拼接"},
|
||||||
|
},
|
||||||
|
"required": ["ids"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.delete(f"/system/mqtt/config/{args['ids']}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("refresh_mqtt_config_cache")
|
||||||
|
class RefreshMqttConfigCacheTool(ToolDef):
|
||||||
|
name = "refresh_mqtt_config_cache"
|
||||||
|
description = "刷新 MQTT 同步配置缓存"
|
||||||
|
input_schema = {"type": "object", "properties": {}, "required": []}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.get("/system/mqtt/config/refreshCache")
|
||||||
386
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py
Normal file
386
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py
Normal file
@@ -0,0 +1,386 @@
|
|||||||
|
"""
|
||||||
|
技能与工具管理工具
|
||||||
|
|
||||||
|
把 SQL 沉淀为数据源可复用工具的入口统一走 add_sql_tool_to_datasource(保证技能必有工具,
|
||||||
|
内部按需建技能+写配置+建工具)。其余为底层积木:查询类、向已有技能加/改工具、改技能配置。
|
||||||
|
不单独暴露「只建技能」工具,避免产生无效空技能。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_skill_by_datasource")
|
||||||
|
class GetSkillByDatasourceTool(ToolDef):
|
||||||
|
name = "get_skill_by_datasource"
|
||||||
|
description = "根据数据源获取技能信息"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.get(f"/datasource/skill/getByDatasource/{args['datasourceId']}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("get_skill_tools")
|
||||||
|
class GetSkillToolsTool(ToolDef):
|
||||||
|
name = "get_skill_tools"
|
||||||
|
description = "获取技能下的工具列表"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"skillId": {"type": "string", "description": "技能 ID"},
|
||||||
|
},
|
||||||
|
"required": ["skillId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.get(f"/datasource/skill/getBySkillId/{args['skillId']}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("create_sql_tool")
|
||||||
|
class CreateSqlToolTool(ToolDef):
|
||||||
|
name = "create_sql_tool"
|
||||||
|
description = "将 SQL 查询创建为可复用工具(支持批量)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"skillId": {"type": "string", "description": "技能 ID"},
|
||||||
|
"tableIds": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "关联的表 ID 数组",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
},
|
||||||
|
"suggestions": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "SQL 工具建议数组",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"name": {"type": "string", "description": "工具名称"},
|
||||||
|
"businessDescription": {"type": "string", "description": "业务描述"},
|
||||||
|
"sqlTemplate": {"type": "string", "description": "SQL 模板(支持 #{param} 参数占位)"},
|
||||||
|
"sqlParams": {"type": "string", "description": "参数定义,最终以 JSON 字符串提交(传 dict 会自动 json.dumps)。内容形态后端不挑剔:可为 JSON Schema 对象串如 {\"type\":\"object\",...},也可为字段定义数组串;无参数传 \"{}\""},
|
||||||
|
"resultType": {"type": "string", "enum": ["single", "list"], "default": "list", "description": "结果类型,默认 list"},
|
||||||
|
"businessScenario": {"type": "string", "description": "业务场景描述"},
|
||||||
|
},
|
||||||
|
"required": ["name", "businessDescription", "sqlTemplate"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["skillId", "suggestions"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
# tableIds 后端始终期望该键存在:前端真机始终传 ""(空串)。
|
||||||
|
# 调用方未给时补 "",与前端 postSqlSkillConfirmTools 的请求保持一致。
|
||||||
|
if "tableIds" not in args or args["tableIds"] is None:
|
||||||
|
args["tableIds"] = ""
|
||||||
|
# 处理 suggestions 中的 sqlParams:dict 自动序列化为 JSON 字符串;
|
||||||
|
# 同时补齐 resultType 默认值 list(与前端默认一致)。
|
||||||
|
if "suggestions" in args and isinstance(args["suggestions"], list):
|
||||||
|
for suggestion in args["suggestions"]:
|
||||||
|
if "sqlParams" in suggestion and isinstance(suggestion["sqlParams"], dict):
|
||||||
|
suggestion["sqlParams"] = json.dumps(suggestion["sqlParams"])
|
||||||
|
suggestion.setdefault("resultType", "list")
|
||||||
|
return await self.client.post("/datasource/skill/confirmTools", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("delete_skill_tool")
|
||||||
|
class DeleteSkillToolTool(ToolDef):
|
||||||
|
name = "delete_skill_tool"
|
||||||
|
description = "删除技能下的工具"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"skillToolId": {"type": "string", "description": "工具 ID"},
|
||||||
|
},
|
||||||
|
"required": ["skillToolId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
return await self.client.delete(f"/datasource/skill/tskilltool/{args['skillToolId']}")
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("update_skill_config")
|
||||||
|
class UpdateSkillConfigTool(ToolDef):
|
||||||
|
name = "update_skill_config"
|
||||||
|
description = (
|
||||||
|
"更新技能(名称/描述/MCP 配置模板等)。对应后端 skill/updateOrGet。"
|
||||||
|
"datasourceId 与 skillId 均必填且为真实 ID(来自其他工具返回,不可臆造)。"
|
||||||
|
"若不显式传 configTemplate,会按这两个 ID 自动生成 lzwcai-mcp-sqlexecutor 的标准 MCP 配置模板(与前端一致)。"
|
||||||
|
)
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "数据源/配置 ID(真实 ID,来自 list_databases / list_tables_with_ai / get_connection_config_list,不可臆造)",
|
||||||
|
},
|
||||||
|
"skillId": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "技能 ID(真实 ID,来自 get_skill_by_datasource 的返回,不可臆造;与 datasourceId 一起用于生成 configTemplate)",
|
||||||
|
},
|
||||||
|
"name": {"type": "string", "description": "技能名称(可选)"},
|
||||||
|
"description": {"type": "string", "description": "技能描述(可选)"},
|
||||||
|
"configTemplate": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "配置模板 JSON 字符串(可选)。不传时按 datasourceId + skillId 自动生成标准模板",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["datasourceId", "skillId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _build_config_template(datasource_id: str, skill_id: str) -> str:
|
||||||
|
"""生成 lzwcai-mcp-sqlexecutor 的标准 MCP 配置模板。
|
||||||
|
|
||||||
|
模板大部分是固定值,仅 mcpServerKey 后缀、env.databaseId、env.skillId 随
|
||||||
|
datasourceId / skillId 动态变化(与前端 SqlControllerMsg.vue 的 configTemplateObj 完全一致)。
|
||||||
|
"""
|
||||||
|
mcp_server_key = f"lzwcai_mcp_sqlexecutor_{datasource_id}"
|
||||||
|
config_obj = {
|
||||||
|
"mcpServers": {
|
||||||
|
mcp_server_key: {
|
||||||
|
"command": "uvx",
|
||||||
|
"type": "stdio",
|
||||||
|
"args": ["lzwcai-mcp-sqlexecutor"],
|
||||||
|
"tiemout": 200,
|
||||||
|
"env": {
|
||||||
|
"databaseId": datasource_id,
|
||||||
|
"skillId": skill_id,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return json.dumps(config_obj)
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
# 如果 configTemplate 是 dict,转为 JSON 字符串
|
||||||
|
if "configTemplate" in args and isinstance(args["configTemplate"], dict):
|
||||||
|
args["configTemplate"] = json.dumps(args["configTemplate"])
|
||||||
|
# 未显式提供 configTemplate 时,按 datasourceId + skillId 自动生成标准模板
|
||||||
|
elif not args.get("configTemplate"):
|
||||||
|
args["configTemplate"] = self._build_config_template(
|
||||||
|
str(args["datasourceId"]), str(args["skillId"])
|
||||||
|
)
|
||||||
|
return await self.client.post("/datasource/skill/updateOrGet", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("update_skill_tool")
|
||||||
|
class UpdateSkillToolTool(ToolDef):
|
||||||
|
name = "update_skill_tool"
|
||||||
|
description = (
|
||||||
|
"修改技能下某个工具的名称/描述/SQL等(对应后端 tskilltool/updateOrGet,upsert 语义)。"
|
||||||
|
"改展示名传 name 即可(工具会同时写 name 和 uniqueName 两个字段,与工具实体存储一致)。"
|
||||||
|
"工具名建议遵循前端约束:≤20 字、只含中英文/数字/空格、不含特殊符号。"
|
||||||
|
)
|
||||||
|
# 工具实体同时存在 name 与 uniqueName 两个字段(真机数据里二者取值相同,见 skill.json)。
|
||||||
|
# 前端 ChatDebugging.vue handleEditToolSubmit 改名时提交 {id, name, description}(用 name);
|
||||||
|
# 早期真机探测又得到 uniqueName。为消除歧义、与实体存储保持一致,改名时两个字段都写。
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "技能工具 ID(get_skill_tools 返回的 id)"},
|
||||||
|
"name": {"type": "string", "description": "工具展示名(可选)。会同时写入 name 与 uniqueName"},
|
||||||
|
"description": {"type": "string", "description": "工具描述(可选)"},
|
||||||
|
"sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"},
|
||||||
|
"resultType": {"type": "string", "enum": ["single", "list"], "description": "结果类型(可选)"},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 旧参数名 -> 真实字段名(向后兼容此前错误实现的调用方)
|
||||||
|
_LEGACY_MAP = {
|
||||||
|
"skillToolId": "id",
|
||||||
|
"businessDescription": "description",
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
for old, new in self._LEGACY_MAP.items():
|
||||||
|
if old in args and new not in args:
|
||||||
|
args[new] = args.pop(old)
|
||||||
|
else:
|
||||||
|
args.pop(old, None)
|
||||||
|
# 展示名:name / uniqueName 任一传入都同步到两个字段(与工具实体存储一致,
|
||||||
|
# 兼容前端用 name、早期探测用 uniqueName 两种契约,避免改名不生效)。
|
||||||
|
display_name = args.get("name") if args.get("name") is not None else args.get("uniqueName")
|
||||||
|
if display_name is not None:
|
||||||
|
args["name"] = display_name
|
||||||
|
args["uniqueName"] = display_name
|
||||||
|
# businessScenario 后端实体无此字段,丢弃避免干扰
|
||||||
|
args.pop("businessScenario", None)
|
||||||
|
return await self.client.post("/datasource/skill/tskilltool/updateOrGet", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("add_sql_tool_to_datasource")
|
||||||
|
class AddSqlToolToDatasourceTool(ToolDef):
|
||||||
|
name = "add_sql_tool_to_datasource"
|
||||||
|
description = (
|
||||||
|
"把一条 SQL 沉淀为数据源的可复用工具(一步到位,推荐用这个而不是手动拼 "
|
||||||
|
"update_skill_config/create_sql_tool)。\n"
|
||||||
|
"【为什么用它】技能(skill)必须挂着工具才有效,单独建技能会留下无效的空技能。本工具内部"
|
||||||
|
"1:1 复刻前端 handleAddToolSubmit 的完整链路,保证技能必有工具:\n"
|
||||||
|
" 读 skillBool → 技能不存在则 createOrGet 建技能 → getByDatasource 拿真实 skillId →"
|
||||||
|
" 按 sqlTemplate 去重 → 技能新建时写 lzwcai-mcp-sqlexecutor 配置模板 → confirmTools 建工具。\n"
|
||||||
|
"【幂等/去重】同一 datasourceId 下若已存在 sqlTemplate 相同的工具,直接返回 skipped,不重复创建。\n"
|
||||||
|
"datasourceId 必填且为真实 ID(来自 list_databases / list_tables_with_ai / get_connection_config_list)。"
|
||||||
|
)
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "数据源/配置 ID(真实 ID,不可臆造)"},
|
||||||
|
"name": {"type": "string", "description": "工具名称(展示名)"},
|
||||||
|
"businessDescription": {"type": "string", "description": "工具的业务描述"},
|
||||||
|
"sqlTemplate": {"type": "string", "description": "SQL 模板(支持 #{param} 参数占位)"},
|
||||||
|
"sqlParams": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "参数定义,最终以 JSON 字符串提交(传 dict 会自动 json.dumps)。内容形态后端不挑剔(JSON Schema 对象串或字段定义数组串均可);不传默认空 schema",
|
||||||
|
},
|
||||||
|
"resultType": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": ["single", "list"],
|
||||||
|
"default": "list",
|
||||||
|
"description": "结果类型,默认 list",
|
||||||
|
},
|
||||||
|
"businessScenario": {"type": "string", "description": "业务场景描述(可选)"},
|
||||||
|
"tableIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": "关联的表 ID 数组(可选)",
|
||||||
|
},
|
||||||
|
"skillName": {"type": "string", "description": "技能不存在时新建技能用的名称(可选,不传自动生成)"},
|
||||||
|
"skillDescription": {"type": "string", "description": "技能不存在时新建技能用的描述(可选)"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceId", "name", "businessDescription", "sqlTemplate"],
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _unwrap(resp):
|
||||||
|
"""从 {code,msg,data} 信封里取 data;非信封则原样返回。"""
|
||||||
|
if isinstance(resp, dict) and "data" in resp and ("code" in resp or "msg" in resp):
|
||||||
|
return resp["data"]
|
||||||
|
return resp
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_sql(sql) -> str:
|
||||||
|
"""归一化 SQL 用于去重比较:折叠空白 + strip(与前端 replace(/\\s+/g,' ').trim() 一致)。"""
|
||||||
|
if not isinstance(sql, str):
|
||||||
|
return ""
|
||||||
|
return " ".join(sql.split()).strip()
|
||||||
|
|
||||||
|
async def _get_skill_id(self, datasource_id: str):
|
||||||
|
"""getByDatasource 拿技能 id;拿不到返回 None。"""
|
||||||
|
resp = await self.client.get(f"/datasource/skill/getByDatasource/{datasource_id}")
|
||||||
|
data = self._unwrap(resp)
|
||||||
|
if isinstance(data, dict):
|
||||||
|
return data.get("id")
|
||||||
|
return None
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
datasource_id = str(args["datasourceId"])
|
||||||
|
sql_template = args["sqlTemplate"]
|
||||||
|
|
||||||
|
# 1. 读数据源配置,判断 skillBool(技能是否已存在)
|
||||||
|
config_resp = await self.client.get(f"/datasource/config/{datasource_id}")
|
||||||
|
config_data = self._unwrap(config_resp)
|
||||||
|
skill_bool = config_data.get("skillBool") if isinstance(config_data, dict) else None
|
||||||
|
|
||||||
|
created_skill = False
|
||||||
|
# 2. 技能不存在 → 先建技能(createOrGet)
|
||||||
|
if skill_bool is not True:
|
||||||
|
create_skill_body = {"datasourceId": datasource_id}
|
||||||
|
if args.get("skillName"):
|
||||||
|
create_skill_body["name"] = args["skillName"]
|
||||||
|
if args.get("skillDescription"):
|
||||||
|
create_skill_body["description"] = args["skillDescription"]
|
||||||
|
await self.client.post("/datasource/skill/createOrGet", json_data=create_skill_body)
|
||||||
|
created_skill = True
|
||||||
|
|
||||||
|
# 3. 拿真实 skillId(注意:id 来自 getByDatasource,不是 createOrGet 的返回)
|
||||||
|
skill_id = await self._get_skill_id(datasource_id)
|
||||||
|
if not skill_id:
|
||||||
|
raise ValueError(
|
||||||
|
f"未能获取数据源 {datasource_id} 的技能 ID(getByDatasource 未返回 id)。"
|
||||||
|
"请确认 datasourceId 正确、技能创建是否成功。"
|
||||||
|
)
|
||||||
|
skill_id = str(skill_id)
|
||||||
|
|
||||||
|
# 4. 去重:同 sqlTemplate 的工具已存在则跳过
|
||||||
|
tools_resp = await self.client.get(f"/datasource/skill/getBySkillId/{skill_id}")
|
||||||
|
tools_data = self._unwrap(tools_resp)
|
||||||
|
target_norm = self._normalize_sql(sql_template)
|
||||||
|
if isinstance(tools_data, list):
|
||||||
|
for tool in tools_data:
|
||||||
|
if isinstance(tool, dict) and self._normalize_sql(tool.get("sqlTemplate")) == target_norm:
|
||||||
|
return {
|
||||||
|
"skipped": True,
|
||||||
|
"reason": "已存在 sqlTemplate 相同的工具,未重复创建",
|
||||||
|
"skillId": skill_id,
|
||||||
|
"existingTool": {
|
||||||
|
"id": tool.get("id"),
|
||||||
|
"uniqueName": tool.get("uniqueName") or tool.get("name"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
# 5. 技能是本次新建的 → 写入 lzwcai-mcp-sqlexecutor 标准配置模板
|
||||||
|
if created_skill:
|
||||||
|
config_template = UpdateSkillConfigTool._build_config_template(datasource_id, skill_id)
|
||||||
|
await self.client.post(
|
||||||
|
"/datasource/skill/updateOrGet",
|
||||||
|
json_data={"datasourceId": datasource_id, "configTemplate": config_template},
|
||||||
|
)
|
||||||
|
|
||||||
|
# 6. confirmTools 建工具
|
||||||
|
sql_params = args.get("sqlParams")
|
||||||
|
if isinstance(sql_params, dict):
|
||||||
|
sql_params = json.dumps(sql_params)
|
||||||
|
elif not sql_params:
|
||||||
|
sql_params = '{"type":"object","required":[],"properties":{}}'
|
||||||
|
|
||||||
|
suggestion = {
|
||||||
|
"name": args["name"],
|
||||||
|
"businessDescription": args["businessDescription"],
|
||||||
|
"sqlTemplate": sql_template,
|
||||||
|
"sqlParams": sql_params,
|
||||||
|
"resultType": args.get("resultType", "list"),
|
||||||
|
"businessScenario": args.get("businessScenario", "数据查询场景"),
|
||||||
|
}
|
||||||
|
# tableIds:前端真机始终传 ""(空串)。None / 空列表都归一为 "",与前端一致;
|
||||||
|
# 仅当调用方显式给了非空列表时才透传该列表。
|
||||||
|
table_ids = args.get("tableIds")
|
||||||
|
confirm_body = {
|
||||||
|
"skillId": skill_id,
|
||||||
|
"tableIds": table_ids if table_ids else "",
|
||||||
|
"suggestions": [suggestion],
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
confirm_result = await self.client.post(
|
||||||
|
"/datasource/skill/confirmTools", json_data=confirm_body
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
# 工具创建失败:若本次刚建了技能,此刻技能是「空技能」(有配置无工具)——
|
||||||
|
# 正是要避免的无效状态。明确告知调用方:技能已建好,重跑本工具即可补上工具
|
||||||
|
# (重跑会走 skillBool=true 分支,仅补工具,幂等安全)。
|
||||||
|
if created_skill:
|
||||||
|
raise Exception(
|
||||||
|
f"技能已创建(skillId={skill_id})但工具创建失败:{e}。"
|
||||||
|
"当前技能为「空技能」,请用相同参数重新调用本工具补上工具"
|
||||||
|
"(重跑只会补工具、不会重复建技能)。"
|
||||||
|
) from e
|
||||||
|
raise
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"skillId": skill_id,
|
||||||
|
"skillCreated": created_skill,
|
||||||
|
"result": confirm_result,
|
||||||
|
}
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
"""
|
||||||
|
SQL 执行工具 (工具 33)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("execute_sql")
|
||||||
|
class ExecuteSqlTool(ToolDef):
|
||||||
|
name = "execute_sql"
|
||||||
|
description = "执行原生 SQL 查询"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||||
|
"sql": {"type": "string", "description": "SQL 语句"},
|
||||||
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
||||||
|
"sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"},
|
||||||
|
"businessName": {"type": "string", "description": "业务名称(可选)"},
|
||||||
|
"params": {"type": "object", "description": "SQL 参数对象(可选)"},
|
||||||
|
},
|
||||||
|
"required": ["datasourceId", "sql"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
# 映射参数名为后端 API 期望的格式
|
||||||
|
body = {}
|
||||||
|
if "datasourceId" in args:
|
||||||
|
body["datasourceId"] = args["datasourceId"]
|
||||||
|
if "sql" in args:
|
||||||
|
body["executableSql"] = args["sql"]
|
||||||
|
if "sqlTemplate" in args:
|
||||||
|
body["sqlTemplate"] = args["sqlTemplate"]
|
||||||
|
if "businessName" in args:
|
||||||
|
body["businessName"] = args["businessName"]
|
||||||
|
if "params" in args:
|
||||||
|
body["parameters"] = args["params"]
|
||||||
|
# target 通过 query 参数下发(prod/test 双环境),默认 prod
|
||||||
|
target = args.get("target", "prod")
|
||||||
|
params = {"target": target} if target else None
|
||||||
|
return await self.client.post(
|
||||||
|
"/datasource/sqlExecutionLog/testSqlWithSchema",
|
||||||
|
json_data=body,
|
||||||
|
params=params,
|
||||||
|
)
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""
|
||||||
|
表订阅工具 (工具 32)
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("toggle_table_subscription")
|
||||||
|
class ToggleTableSubscriptionTool(ToolDef):
|
||||||
|
name = "toggle_table_subscription"
|
||||||
|
description = "切换表的订阅状态(订阅依赖该表已配置 MQTT 字段关联,否则后端会报操作失败)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"configId": {"type": "string", "description": "库/配置 ID(即 list_databases 返回的 config id)"},
|
||||||
|
"tableName": {"type": "string", "description": "表名(注意:后端按表名而非 tableId 识别)"},
|
||||||
|
"subscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"},
|
||||||
|
},
|
||||||
|
"required": ["configId", "tableName", "subscribe"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
# 后端真实字段(已真机探测确认):configId + tableName + isSubscribe(bool)
|
||||||
|
# 兼容旧参数名 datasourceId->configId
|
||||||
|
config_id = args.get("configId") or args.get("datasourceId")
|
||||||
|
body = {
|
||||||
|
"configId": config_id,
|
||||||
|
"tableName": args.get("tableName"),
|
||||||
|
"isSubscribe": args.get("subscribe"),
|
||||||
|
}
|
||||||
|
return await self.client.post("/datasource/subscription/toggle", json_data=body)
|
||||||
139
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py
Normal file
139
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
"""
|
||||||
|
表数据 CRUD 工具 (工具 13-17)
|
||||||
|
"""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
|
||||||
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("query_table_data")
|
||||||
|
class QueryTableDataTool(ToolDef):
|
||||||
|
name = "query_table_data"
|
||||||
|
description = "查询内置表数据(分页)。target 默认 test(调试环境);查询线上数据须显式传 target=prod"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test(调试);线上数据传 prod"},
|
||||||
|
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||||
|
"pageSize": {"type": "integer", "default": 10, "description": "每页数量"},
|
||||||
|
},
|
||||||
|
"required": ["tableId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
table_id = args.pop("tableId")
|
||||||
|
args.setdefault("target", "test")
|
||||||
|
params = {k: v for k, v in args.items() if v is not None}
|
||||||
|
return await self.client.get(f"/datasource/connection/builtin/table/{table_id}", params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("insert_table_row")
|
||||||
|
class InsertTableRowTool(ToolDef):
|
||||||
|
name = "insert_table_row"
|
||||||
|
description = "向内置表插入一行数据。⚠️ 写操作 target 默认 test(调试环境);写入线上库须显式传 target=prod"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test(调试);写入线上库必须显式传 prod"},
|
||||||
|
"data": {"type": "object", "description": "行数据(键值对,键为字段名)"},
|
||||||
|
},
|
||||||
|
"required": ["tableId", "data"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
table_id = args.pop("tableId")
|
||||||
|
target = args.pop("target", "test")
|
||||||
|
data = args.pop("data", {})
|
||||||
|
params = {"target": target} if target else {}
|
||||||
|
body = {"data": data}
|
||||||
|
return await self.client.post(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("update_table_row")
|
||||||
|
class UpdateTableRowTool(ToolDef):
|
||||||
|
name = "update_table_row"
|
||||||
|
description = "更新内置表的指定行。⚠️ 写操作 target 默认 test(调试环境);修改线上库须显式传 target=prod"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test(调试);修改线上库必须显式传 prod"},
|
||||||
|
"primaryKey": {"type": "object", "description": "主键值(如 {\"id\": 1})"},
|
||||||
|
"data": {"type": "object", "description": "要更新的字段值"},
|
||||||
|
},
|
||||||
|
"required": ["tableId", "primaryKey", "data"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
table_id = args.pop("tableId")
|
||||||
|
target = args.pop("target", "test")
|
||||||
|
primary_key = args.pop("primaryKey")
|
||||||
|
data = args.pop("data", {})
|
||||||
|
params = {"target": target} if target else {}
|
||||||
|
body = {"primaryKey": primary_key, "data": data}
|
||||||
|
return await self.client.put(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("delete_table_rows")
|
||||||
|
class DeleteTableRowsTool(ToolDef):
|
||||||
|
name = "delete_table_rows"
|
||||||
|
description = "删除内置表的指定行(根据主键批量删除)。⚠️ 写操作 target 默认 test(调试环境);删除线上库数据须显式传 target=prod"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test(调试);删除线上库数据必须显式传 prod"},
|
||||||
|
"primaryKeys": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "主键数组(如 [{\"id\": 1}, {\"id\": 2}])",
|
||||||
|
"items": {"type": "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["tableId", "primaryKeys"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
table_id = args.pop("tableId")
|
||||||
|
target = args.pop("target", "test")
|
||||||
|
primary_keys = args.pop("primaryKeys")
|
||||||
|
params = {"target": target} if target else {}
|
||||||
|
body = {"primaryKeys": primary_keys}
|
||||||
|
return await self.client.delete(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("export_table_excel")
|
||||||
|
class ExportTableExcelTool(ToolDef):
|
||||||
|
name = "export_table_excel"
|
||||||
|
description = "导出表数据为 Excel 文件(返回 base64 编码)。target 默认 test(调试环境);导出线上数据须显式传 target=prod"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test(调试);线上数据传 prod"},
|
||||||
|
},
|
||||||
|
"required": ["tableId"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
table_id = args.pop("tableId")
|
||||||
|
target = args.pop("target", "test")
|
||||||
|
params = {"target": target} if target else {}
|
||||||
|
result = await self.client.get(f"/datasource/connection/builtin/table/{table_id}/export/excel", params=params)
|
||||||
|
|
||||||
|
# 处理二进制响应
|
||||||
|
if result.get("raw"):
|
||||||
|
content = result["data"]
|
||||||
|
return {
|
||||||
|
"success": True,
|
||||||
|
"file_base64": base64.b64encode(content).decode("utf-8"),
|
||||||
|
"message": "Excel 文件已导出,请解码 base64 内容获取文件",
|
||||||
|
}
|
||||||
|
return result
|
||||||
1139
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt
Normal file
1139
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt
Normal file
File diff suppressed because it is too large
Load Diff
21
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__init__.py
Normal file
21
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__init__.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
from .env_config import (
|
||||||
|
get_api_key,
|
||||||
|
get_base_url,
|
||||||
|
get_env_config,
|
||||||
|
get_account,
|
||||||
|
get_password,
|
||||||
|
)
|
||||||
|
from .logger_config import setup_system_logging, get_logger
|
||||||
|
from .api_client import AgileDBAPIClient, get_default_client
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
'get_api_key',
|
||||||
|
'get_base_url',
|
||||||
|
'get_env_config',
|
||||||
|
'get_account',
|
||||||
|
'get_password',
|
||||||
|
'setup_system_logging',
|
||||||
|
'get_logger',
|
||||||
|
'AgileDBAPIClient',
|
||||||
|
'get_default_client',
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
381
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py
Normal file
381
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
"""
|
||||||
|
数据库管理平台 API 调用客户端
|
||||||
|
用于调用数据库管理平台的所有 API 接口
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import httpx
|
||||||
|
import json
|
||||||
|
from typing import Dict, Any, Optional
|
||||||
|
|
||||||
|
from .env_config import (
|
||||||
|
get_api_key,
|
||||||
|
get_base_url,
|
||||||
|
get_account,
|
||||||
|
get_password,
|
||||||
|
)
|
||||||
|
from .logger_config import get_logger
|
||||||
|
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# 默认超时配置(秒)
|
||||||
|
DEFAULT_TIMEOUT = 30.0
|
||||||
|
|
||||||
|
# 登录接口路径(base_url 已含 /api 前缀,此处不重复带)
|
||||||
|
LOGIN_PATH = "/login"
|
||||||
|
|
||||||
|
# 登录类型(平台固定为 user)
|
||||||
|
LOGIN_TYPE = "user"
|
||||||
|
|
||||||
|
|
||||||
|
class AgileDBAPIClient:
|
||||||
|
"""数据库管理平台 API 客户端
|
||||||
|
|
||||||
|
认证支持两种方式(优先级从高到低):
|
||||||
|
1. 显式 api_key / 环境变量 API_KEY —— 直接作为 Bearer token 使用;
|
||||||
|
2. 账号密码(环境变量 AGILE_DB_ACCOUNT / AGILE_DB_PASSWORD)—— 懒登录,
|
||||||
|
首次请求时自动调用 /login 换取 token 并缓存;token 失效(401)时
|
||||||
|
自动重新登录并重试一次。
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
account: Optional[str] = None,
|
||||||
|
password: Optional[str] = None,
|
||||||
|
default_timeout: float = DEFAULT_TIMEOUT,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
初始化 API 客户端
|
||||||
|
|
||||||
|
Args:
|
||||||
|
base_url: API 基础 URL(默认从环境变量 backendBaseUrl 读取)
|
||||||
|
api_key: API 密钥(默认从环境变量 API_KEY 读取,可为空)
|
||||||
|
account: 登录账号(默认从环境变量 AGILE_DB_ACCOUNT 读取)
|
||||||
|
password: 登录密码(默认从环境变量 AGILE_DB_PASSWORD 读取)
|
||||||
|
default_timeout: 请求超时时间(秒),默认 30 秒
|
||||||
|
"""
|
||||||
|
self.base_url = (base_url if base_url is not None else get_base_url()).rstrip('/')
|
||||||
|
# 显式配置的 api_key 直接作为 token 使用(去掉可能存在的 Bearer 前缀,统一在 _get_headers 拼)
|
||||||
|
explicit_key = api_key if api_key is not None else get_api_key()
|
||||||
|
self._token: Optional[str] = self._strip_bearer(explicit_key) or None
|
||||||
|
|
||||||
|
self.account = account if account is not None else get_account()
|
||||||
|
self.password = password if password is not None else get_password()
|
||||||
|
|
||||||
|
self.default_timeout = default_timeout
|
||||||
|
self._client: Optional[httpx.AsyncClient] = None
|
||||||
|
# 串行化登录,避免并发请求同时触发多次登录
|
||||||
|
self._login_lock = asyncio.Lock()
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"[客户端初始化] base_url={self.base_url}, "
|
||||||
|
f"认证方式={'api_key' if self._token else ('account:' + self.account if self.account else '未配置')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _strip_bearer(value: Optional[str]) -> str:
|
||||||
|
"""去掉 token 字符串可能携带的 'Bearer ' 前缀"""
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
value = value.strip()
|
||||||
|
return value[7:].strip() if value.lower().startswith("bearer ") else value
|
||||||
|
|
||||||
|
@property
|
||||||
|
def client(self) -> httpx.AsyncClient:
|
||||||
|
"""懒加载 HTTP 客户端"""
|
||||||
|
if self._client is None:
|
||||||
|
self._client = httpx.AsyncClient(timeout=self.default_timeout)
|
||||||
|
return self._client
|
||||||
|
|
||||||
|
async def _ensure_token(self) -> str:
|
||||||
|
"""确保已有可用 token,没有则登录获取"""
|
||||||
|
if self._token:
|
||||||
|
return self._token
|
||||||
|
return await self._login()
|
||||||
|
|
||||||
|
async def _login(self) -> str:
|
||||||
|
"""换取 token 并缓存(并发安全,用于首次登录)"""
|
||||||
|
async with self._login_lock:
|
||||||
|
# 双重检查:可能在等锁期间已有其它协程完成登录
|
||||||
|
if self._token:
|
||||||
|
return self._token
|
||||||
|
return await self._do_login()
|
||||||
|
|
||||||
|
async def _relogin(self, stale_token: Optional[str]) -> str:
|
||||||
|
"""登录态失效后重新登录(compare-and-swap,并发安全)。
|
||||||
|
|
||||||
|
仅当当前 token 仍是那次失败请求所用的旧 token 时才真正重登;
|
||||||
|
若在等锁期间已有其它协程刷新过 token,则直接复用新 token,
|
||||||
|
避免把别人刚拿到的新 token 抹掉又触发一次多余的重登。
|
||||||
|
"""
|
||||||
|
async with self._login_lock:
|
||||||
|
if self._token != stale_token:
|
||||||
|
# 别的协程已经刷新过 token,直接用新的
|
||||||
|
return self._token or ""
|
||||||
|
self._token = None
|
||||||
|
return await self._do_login()
|
||||||
|
|
||||||
|
async def _do_login(self) -> str:
|
||||||
|
"""实际执行 /login 的逻辑(调用方需自行持有 _login_lock)。"""
|
||||||
|
if not self.account or not self.password:
|
||||||
|
raise Exception(
|
||||||
|
"未配置认证信息:请设置环境变量 API_KEY,"
|
||||||
|
"或同时设置 AGILE_DB_ACCOUNT 和 AGILE_DB_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
url = self._build_url(LOGIN_PATH)
|
||||||
|
payload = {
|
||||||
|
"username": self.account,
|
||||||
|
"password": self.password,
|
||||||
|
"loginType": LOGIN_TYPE,
|
||||||
|
}
|
||||||
|
logger.info(f"[登录] POST {url}, username={self.account}, loginType={LOGIN_TYPE}")
|
||||||
|
try:
|
||||||
|
response = await self.client.post(
|
||||||
|
url,
|
||||||
|
headers={"Content-Type": "application/json"},
|
||||||
|
json=payload,
|
||||||
|
)
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
raise Exception(f"登录请求超时: {url}")
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
raise Exception(f"登录请求异常: {url}, 错误: {str(e)}")
|
||||||
|
|
||||||
|
is_json, body = self._try_parse_json(response)
|
||||||
|
data = self._handle_response(response, url, is_json, body)
|
||||||
|
# 平台登录响应:token 在顶层 token 字段
|
||||||
|
token = data.get("token") if isinstance(data, dict) else None
|
||||||
|
if not token:
|
||||||
|
raise Exception(f"登录成功但未返回 token,响应: {json.dumps(data, ensure_ascii=False)[:300]}")
|
||||||
|
|
||||||
|
self._token = self._strip_bearer(token)
|
||||||
|
logger.info("[登录] 成功获取 token")
|
||||||
|
return self._token
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _try_parse_json(response: httpx.Response):
|
||||||
|
"""尝试把响应体解析为 JSON,只解析一次供后续复用。
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
(is_json, data):是 JSON 则 (True, 解析结果);
|
||||||
|
二进制/非 JSON 响应(如 Excel 下载)则 (False, None)。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return True, response.json()
|
||||||
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
|
return False, None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _is_unauthorized(response: httpx.Response, is_json: bool, data: Any) -> bool:
|
||||||
|
"""判断响应是否为登录态失效(基于已解析好的 body,不重复 parse)。
|
||||||
|
|
||||||
|
平台有两种表达 401 的方式,都需识别:
|
||||||
|
1. HTTP 状态码 401;
|
||||||
|
2. HTTP 200 但 body 信封里 code=401(如 {"code":401,"msg":"登录过期,请重新登录"})。
|
||||||
|
"""
|
||||||
|
if response.status_code == 401:
|
||||||
|
return True
|
||||||
|
return is_json and isinstance(data, dict) and data.get("code") == 401
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _rewind_files(files: Optional[Dict[str, Any]]) -> None:
|
||||||
|
"""把上传用的文件流游标重置到开头。
|
||||||
|
|
||||||
|
401 重试会复用同一个 files 二次发送,而文件流在第一次发送后游标已到末尾,
|
||||||
|
不 rewind 会导致重试上传空内容。支持两种形态:
|
||||||
|
- 直接的文件对象;
|
||||||
|
- (filename, fileobj, content_type) 元组(httpx multipart 常用写法)。
|
||||||
|
"""
|
||||||
|
if not files:
|
||||||
|
return
|
||||||
|
for value in files.values():
|
||||||
|
fileobj = value
|
||||||
|
if isinstance(value, (tuple, list)) and len(value) >= 2:
|
||||||
|
fileobj = value[1]
|
||||||
|
seek = getattr(fileobj, "seek", None)
|
||||||
|
if callable(seek):
|
||||||
|
try:
|
||||||
|
seek(0)
|
||||||
|
except (OSError, ValueError):
|
||||||
|
# 不可重置的流(如已关闭/不支持 seek)静默跳过,交由上传结果反映
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _get_headers(self, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
||||||
|
"""获取请求头"""
|
||||||
|
headers = {}
|
||||||
|
if self._token:
|
||||||
|
headers['Authorization'] = f'Bearer {self._token}'
|
||||||
|
if extra_headers:
|
||||||
|
headers.update(extra_headers)
|
||||||
|
return headers
|
||||||
|
|
||||||
|
def _build_url(self, path: str) -> str:
|
||||||
|
"""构建完整 URL
|
||||||
|
|
||||||
|
约定:base_url 已包含完整地址(如含 /api 前缀则配在环境变量里),
|
||||||
|
各工具传入的 path 不带 /api 前缀,此处直接拼接。
|
||||||
|
"""
|
||||||
|
if path.startswith('http://') or path.startswith('https://'):
|
||||||
|
return path
|
||||||
|
return f"{self.base_url}{path}"
|
||||||
|
|
||||||
|
def _handle_response(
|
||||||
|
self,
|
||||||
|
response: httpx.Response,
|
||||||
|
url: str,
|
||||||
|
is_json: Optional[bool] = None,
|
||||||
|
data: Any = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""统一处理 API 响应
|
||||||
|
|
||||||
|
is_json / data 为调用方已解析好的 body(避免对大响应重复 parse);
|
||||||
|
未传入时此处自行解析一次。
|
||||||
|
"""
|
||||||
|
logger.info(f"[API响应] HTTP {response.status_code}")
|
||||||
|
|
||||||
|
if response.status_code == 204:
|
||||||
|
return {"success": True, "data": None}
|
||||||
|
|
||||||
|
# 复用调用方解析结果;未提供则在此解析一次
|
||||||
|
if is_json is None:
|
||||||
|
is_json, data = self._try_parse_json(response)
|
||||||
|
|
||||||
|
# 先看 body 再判断状态码。
|
||||||
|
# 平台会用非 2xx 状态码 + body 里的 {code, msg} 返回业务错误,
|
||||||
|
# 若先 raise_for_status() 会在拿到 body 前抛异常,导致真正的 msg 全部丢失。
|
||||||
|
if not is_json:
|
||||||
|
# 非 JSON 响应(如 Excel/文件下载,二进制内容 .json() 会抛 UnicodeDecodeError)
|
||||||
|
response.raise_for_status()
|
||||||
|
return {"success": True, "data": response.content, "raw": True}
|
||||||
|
|
||||||
|
# 检查平台 API 的 {code, msg} 格式
|
||||||
|
# 平台约定:code 为 200 或 0 均表示成功(见数据库模块文档 §1.1)
|
||||||
|
if isinstance(data, dict) and 'code' in data:
|
||||||
|
if data['code'] not in (0, 200):
|
||||||
|
error_msg = data.get('msg', '未知错误')
|
||||||
|
logger.error(f"[API错误] HTTP {response.status_code}, {error_msg}")
|
||||||
|
raise Exception(error_msg)
|
||||||
|
# code 合法即视为业务成功,不再用 HTTP 状态码二次否决
|
||||||
|
return data
|
||||||
|
|
||||||
|
# 无 {code} 信封的响应:回落到 HTTP 状态码判断,
|
||||||
|
# 非 2xx 时把 body 文本带进异常,避免错误细节丢失。
|
||||||
|
if response.status_code >= 400:
|
||||||
|
detail = data if isinstance(data, str) else json.dumps(data, ensure_ascii=False)
|
||||||
|
logger.error(f"[API错误] HTTP {response.status_code}, {detail}")
|
||||||
|
raise Exception(f"HTTP {response.status_code}: {detail}")
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
async def _request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
*,
|
||||||
|
params: Optional[Dict[str, Any]] = None,
|
||||||
|
json_data: Optional[Dict[str, Any]] = None,
|
||||||
|
files: Optional[Dict[str, Any]] = None,
|
||||||
|
extra_headers: Optional[Dict[str, str]] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""统一请求入口:自动注入认证、登录态失效(401)时重登重试一次"""
|
||||||
|
url = self._build_url(path)
|
||||||
|
# 账号密码模式下首次请求前先确保有 token;纯 api_key 模式 _ensure_token 直接返回
|
||||||
|
await self._ensure_token()
|
||||||
|
|
||||||
|
async def _send() -> httpx.Response:
|
||||||
|
headers = self._get_headers(extra_headers)
|
||||||
|
return await self.client.request(
|
||||||
|
method, url, headers=headers, params=params, json=json_data, files=files
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
logger.info(f"[API请求] {method} {url}")
|
||||||
|
# 记下本次请求所用 token,供 401 时做 compare-and-swap 重登
|
||||||
|
token_used = self._token
|
||||||
|
response = await _send()
|
||||||
|
is_json, body = self._try_parse_json(response)
|
||||||
|
|
||||||
|
# token 失效:仅在账号密码模式下尝试重新登录并重试一次。
|
||||||
|
# 平台可能用 HTTP 401,也可能用 HTTP 200 + body code=401 表达登录过期,
|
||||||
|
# 两者都要识别(见 _is_unauthorized)。
|
||||||
|
if self._is_unauthorized(response, is_json, body) and self.account and self.password:
|
||||||
|
logger.warning("[认证] 收到 401(登录过期),尝试重新登录后重试一次")
|
||||||
|
# CAS 重登:仅当 token 仍是本次用的旧值才真正重登,否则复用别人刚拿到的新 token
|
||||||
|
await self._relogin(token_used)
|
||||||
|
# 重试前把上传文件流游标重置到开头,避免二次发送空内容
|
||||||
|
self._rewind_files(files)
|
||||||
|
response = await _send()
|
||||||
|
is_json, body = self._try_parse_json(response)
|
||||||
|
|
||||||
|
# 重登后仍判定为登录态失效:账号密码本身失效/被禁,给明确报错
|
||||||
|
if self._is_unauthorized(response, is_json, body):
|
||||||
|
logger.error("[认证] 重新登录后仍返回 401")
|
||||||
|
raise Exception("重新登录后仍未通过认证,请检查账号密码是否正确或账号是否被禁用")
|
||||||
|
|
||||||
|
return self._handle_response(response, url, is_json, body)
|
||||||
|
except httpx.TimeoutException:
|
||||||
|
raise Exception(f"API 请求超时: {url}")
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}")
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
raise Exception(f"API 请求异常: {url}, 错误: {str(e)}")
|
||||||
|
|
||||||
|
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""发送 GET 请求"""
|
||||||
|
return await self._request("GET", path, params=params)
|
||||||
|
|
||||||
|
async def post(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""发送 POST 请求"""
|
||||||
|
return await self._request(
|
||||||
|
"POST", path, params=params, json_data=json_data,
|
||||||
|
extra_headers={'Content-Type': 'application/json'},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def put(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""发送 PUT 请求"""
|
||||||
|
return await self._request(
|
||||||
|
"PUT", path, params=params, json_data=json_data,
|
||||||
|
extra_headers={'Content-Type': 'application/json'},
|
||||||
|
)
|
||||||
|
|
||||||
|
async def delete(self, path: str, params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""发送 DELETE 请求
|
||||||
|
|
||||||
|
平台部分 DELETE 接口需带 body,统一走通用 request() 处理。
|
||||||
|
"""
|
||||||
|
extra = {'Content-Type': 'application/json'} if json_data is not None else None
|
||||||
|
return await self._request("DELETE", path, params=params, json_data=json_data, extra_headers=extra)
|
||||||
|
|
||||||
|
async def upload(self, path: str, files: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||||
|
"""发送文件上传请求(multipart/form-data)
|
||||||
|
|
||||||
|
不显式设置 Content-Type,httpx 会根据 files 自动生成 multipart 边界。
|
||||||
|
"""
|
||||||
|
return await self._request("POST", path, params=params, files=files)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
"""关闭 HTTP 客户端"""
|
||||||
|
if self._client is not None:
|
||||||
|
await self._client.aclose()
|
||||||
|
self._client = None
|
||||||
|
|
||||||
|
async def __aenter__(self):
|
||||||
|
return self
|
||||||
|
|
||||||
|
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||||
|
await self.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
# 懒加载的默认客户端
|
||||||
|
_default_client: Optional[AgileDBAPIClient] = None
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_client() -> AgileDBAPIClient:
|
||||||
|
"""获取默认客户端(懒加载)"""
|
||||||
|
global _default_client
|
||||||
|
if _default_client is None:
|
||||||
|
_default_client = AgileDBAPIClient()
|
||||||
|
return _default_client
|
||||||
68
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py
Normal file
68
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
"""环境变量配置模块 - 数据库管理平台 MCP Server"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_key(default: Optional[str] = None) -> str:
|
||||||
|
"""
|
||||||
|
获取 API 密钥(可选)
|
||||||
|
|
||||||
|
优先级:显式配置了 API_KEY 时直接使用;未配置则返回空串,
|
||||||
|
由客户端回落到账号密码登录流程换取 token。
|
||||||
|
|
||||||
|
Args:
|
||||||
|
default: 默认值(可选)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: API 密钥,未配置时为空串
|
||||||
|
"""
|
||||||
|
return os.environ.get("API_KEY", default or "")
|
||||||
|
|
||||||
|
|
||||||
|
def get_account(default: Optional[str] = None) -> str:
|
||||||
|
"""获取登录账号(环境变量 AGILE_DB_ACCOUNT)"""
|
||||||
|
return os.environ.get("AGILE_DB_ACCOUNT", default or "")
|
||||||
|
|
||||||
|
|
||||||
|
def get_password(default: Optional[str] = None) -> str:
|
||||||
|
"""获取登录密码(环境变量 AGILE_DB_PASSWORD)"""
|
||||||
|
return os.environ.get("AGILE_DB_PASSWORD", default or "")
|
||||||
|
|
||||||
|
|
||||||
|
def get_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str:
|
||||||
|
"""
|
||||||
|
获取后端服务地址
|
||||||
|
|
||||||
|
Args:
|
||||||
|
default: 默认值(默认 http://lzwcai-demp-corp-manager:8086)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
str: 后端 API 基础 URL
|
||||||
|
"""
|
||||||
|
return os.environ.get("backendBaseUrl", default)
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_config() -> dict:
|
||||||
|
"""
|
||||||
|
获取所有环境配置
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
dict: 包含所有配置的字典
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"api_key": os.environ.get("API_KEY", ""),
|
||||||
|
"account": os.environ.get("AGILE_DB_ACCOUNT", ""),
|
||||||
|
"base_url": get_base_url(),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def set_env_variable(key: str, value: str) -> None:
|
||||||
|
"""
|
||||||
|
设置环境变量(仅在当前进程中有效)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key: 环境变量名
|
||||||
|
value: 环境变量值
|
||||||
|
"""
|
||||||
|
os.environ[key] = value
|
||||||
121
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/logger_config.py
Normal file
121
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/logger_config.py
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
统一日志配置模块
|
||||||
|
提供系统级别的日志配置和管理
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerConfig:
|
||||||
|
"""日志配置管理类"""
|
||||||
|
|
||||||
|
def __init__(self, logs_dir: str = None):
|
||||||
|
"""初始化日志配置
|
||||||
|
|
||||||
|
Args:
|
||||||
|
logs_dir: 日志目录路径,默认为项目根目录下的logs文件夹
|
||||||
|
"""
|
||||||
|
if logs_dir:
|
||||||
|
self.logs_dir = Path(logs_dir)
|
||||||
|
else:
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
self.logs_dir = project_root / "logs"
|
||||||
|
|
||||||
|
self.logs_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
self.log_format = '%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s'
|
||||||
|
self.date_format = '%Y-%m-%d %H:%M:%S'
|
||||||
|
self.log_level = self._get_log_level_from_env()
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
|
def _get_log_level_from_env(self) -> int:
|
||||||
|
log_level_str = os.getenv('LOG_LEVEL', 'INFO').upper()
|
||||||
|
level_mapping = {
|
||||||
|
'DEBUG': logging.DEBUG,
|
||||||
|
'INFO': logging.INFO,
|
||||||
|
'WARNING': logging.WARNING,
|
||||||
|
'WARN': logging.WARNING,
|
||||||
|
'ERROR': logging.ERROR,
|
||||||
|
'CRITICAL': logging.CRITICAL,
|
||||||
|
'FATAL': logging.CRITICAL
|
||||||
|
}
|
||||||
|
return level_mapping.get(log_level_str, logging.INFO)
|
||||||
|
|
||||||
|
def setup_logging(self,
|
||||||
|
app_name: str = "lzwcai_mcp_agile_db",
|
||||||
|
log_level: int = logging.INFO,
|
||||||
|
max_file_size: int = 10 * 1024 * 1024,
|
||||||
|
backup_count: int = 5,
|
||||||
|
console_output: bool = True) -> logging.Logger:
|
||||||
|
if self._initialized:
|
||||||
|
return logging.getLogger()
|
||||||
|
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(log_level)
|
||||||
|
|
||||||
|
for handler in root_logger.handlers[:]:
|
||||||
|
root_logger.removeHandler(handler)
|
||||||
|
|
||||||
|
formatter = logging.Formatter(self.log_format, self.date_format)
|
||||||
|
|
||||||
|
# 1. 主日志文件 - 按大小滚动
|
||||||
|
main_log_file = self.logs_dir / f"{app_name}.log"
|
||||||
|
file_handler = RotatingFileHandler(
|
||||||
|
main_log_file,
|
||||||
|
maxBytes=max_file_size,
|
||||||
|
backupCount=backup_count,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
file_handler.setLevel(log_level)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# 2. 错误日志文件
|
||||||
|
error_log_file = self.logs_dir / f"{app_name}_error.log"
|
||||||
|
error_handler = RotatingFileHandler(
|
||||||
|
error_log_file,
|
||||||
|
maxBytes=max_file_size,
|
||||||
|
backupCount=backup_count,
|
||||||
|
encoding='utf-8'
|
||||||
|
)
|
||||||
|
error_handler.setLevel(logging.ERROR)
|
||||||
|
error_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(error_handler)
|
||||||
|
|
||||||
|
# 3. 控制台输出 (MCP协议使用stdio时,必须将日志输出到stderr)
|
||||||
|
if console_output:
|
||||||
|
console_handler = logging.StreamHandler(sys.stderr)
|
||||||
|
console_handler.setLevel(log_level)
|
||||||
|
console_formatter = logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
self.date_format
|
||||||
|
)
|
||||||
|
console_handler.setFormatter(console_formatter)
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
self._initialized = True
|
||||||
|
root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}")
|
||||||
|
|
||||||
|
return root_logger
|
||||||
|
|
||||||
|
def get_module_logger(self, name: str) -> logging.Logger:
|
||||||
|
"""获取模块级别的 logger(继承根配置)"""
|
||||||
|
return logging.getLogger(name)
|
||||||
|
|
||||||
|
|
||||||
|
# 全局日志配置实例
|
||||||
|
logger_config = LoggerConfig()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_system_logging(app_name: str = "lzwcai_mcp_agile_db",
|
||||||
|
log_level: int = logging.INFO) -> logging.Logger:
|
||||||
|
return logger_config.setup_logging(app_name, log_level)
|
||||||
|
|
||||||
|
|
||||||
|
def get_logger(name: str) -> logging.Logger:
|
||||||
|
return logger_config.get_module_logger(name)
|
||||||
454
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库模块-功能与业务链路说明.md
Normal file
454
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库模块-功能与业务链路说明.md
Normal file
@@ -0,0 +1,454 @@
|
|||||||
|
# 数据库模块 —— 功能、接口与业务链路说明
|
||||||
|
|
||||||
|
> 适用范围:`src/components/databasePage/` 全部组件 + `src/server/database.ts` API 层
|
||||||
|
> 数据源分两类:**外置数据源(external)** 与 **内置数据源(builtin / 内置 PostgreSQL)**
|
||||||
|
> 所有接口前缀均为 `/api/datasource`(MQTT 同步相关为 `/api/system/mqtt`)
|
||||||
|
> 大部分接口超时设置为 `300000ms`(5 分钟,因含 AI 处理)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 0. 名词与基础概念
|
||||||
|
|
||||||
|
| 名词 | 说明 |
|
||||||
|
| --- | --- |
|
||||||
|
| 连接 / 数据源(connection) | 一条数据库连接记录,`sourceType` 为 `external` 或 `builtin` |
|
||||||
|
| 配置(config) | 数据源下「数据库 + 选中表」的关联配置,一条 config 对应一个库 |
|
||||||
|
| 表元数据(table metadata) | 平台侧记录的表结构(字段、注释、AI 训练描述等) |
|
||||||
|
| 技能(skill) | 绑定到某数据源的「智能问数技能」 |
|
||||||
|
| 工具(tool) | 技能下的一条可复用 SQL 查询能力(沉淀为 MCP 工具) |
|
||||||
|
| 环境(target) | 内置数据源数据分 `prod`(线上)/ `test`(调试)两套 |
|
||||||
|
| status | 数据源状态:`0` 运行中,`1` 已停止 |
|
||||||
|
|
||||||
|
### 两类数据源核心差异
|
||||||
|
|
||||||
|
| 维度 | 外置数据源 external | 内置数据源 builtin |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 来源 | 连接用户已有的远程数据库 | 平台内置 PostgreSQL,平台直接建库建表 |
|
||||||
|
| 默认数字员工 | `1001`(SQL处理助手) | `1002`(SQL处理助手) |
|
||||||
|
| 建表能力 | ❌ 只读连接,不能建表 | ✅ AI 智能建表 / Excel 导入建表 / 手动建表 |
|
||||||
|
| 数据增删改 | ❌ 不提供行级 CRUD | ✅ 行级增删改查 + Excel 导入导出 |
|
||||||
|
| 环境切换 | ❌ 无 | ✅ prod / test 双环境 |
|
||||||
|
| 表订阅同步 | ❌ | ✅ MQTT 数据变更订阅 |
|
||||||
|
| 创建入口 | `CreateDataSource.vue` | `CreateBuiltinDataSource.vue` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 数据源(连接)的增删改查 —— 内外置通用
|
||||||
|
|
||||||
|
入口:[DataSourcePageMain.vue](../src/components/databasePage/DataSourcePageMain.vue) → [DataSourceList.vue](../src/components/databasePage/DataSourceList.vue)
|
||||||
|
|
||||||
|
### 1.1 查询(列表 / 详情)
|
||||||
|
|
||||||
|
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 数据源列表(分页/搜索/状态筛选) | `fetchDataSources()` | `getConnectionList(params)` | GET | `/api/datasource/connection/list` |
|
||||||
|
| 数据源详情 | `fetchDataSourceDetail()` | `getConnectionDetail(id)` | GET | `/api/datasource/connection/{id}` |
|
||||||
|
| 连接实例详情(含库表结构) | — | `getConnectionInstanceDetail(id)` | GET | `/api/datasource/connection/{id}` |
|
||||||
|
| 查看数据源实时库表结构 | `loadDatabaseList()` | `getConnectionRealtimeStructure(id)` | GET | `/api/datasource/connection/realtime/structure/{id}` |
|
||||||
|
|
||||||
|
- 列表查询参数:`{ datasourceName?, status?, pageNum, pageSize }`,前端用手动 `IntersectionObserver` 做无限滚动。
|
||||||
|
- 响应判定统一:`code === 200 || code === 0` 视为成功,列表数据在 `response.rows`,总数在 `response.total`。
|
||||||
|
|
||||||
|
### 1.2 启用 / 停用
|
||||||
|
|
||||||
|
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 切换运行状态 | `handleChangeStatus()` | `putConnectionChangeStatus({ id, status })` | PUT | `/api/datasource/connection/changeStatus` |
|
||||||
|
|
||||||
|
- `status` 切换:`1->0` 启用,`0->1` 停用。
|
||||||
|
|
||||||
|
### 1.3 删除
|
||||||
|
|
||||||
|
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 删除数据源 | `handleDelete()` | `deleteConnection(id)` | DELETE | `/api/datasource/connection/{id}` |
|
||||||
|
|
||||||
|
**业务链路(删除)**:
|
||||||
|
```
|
||||||
|
用户点删除 → Modal 二次确认
|
||||||
|
→ 若 status===0(运行中):先 putConnectionChangeStatus({id, status:1}) 停用
|
||||||
|
→ 等待 500ms 确保状态更新
|
||||||
|
→ deleteConnection(id)
|
||||||
|
→ 成功后 fetchDataSources() 刷新列表
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 外置数据源(external)的创建与编辑
|
||||||
|
|
||||||
|
入口组件:[CreateDataSource.vue](../src/components/databasePage/CreateDataSource.vue)(4 步向导)
|
||||||
|
|
||||||
|
### 2.1 步骤与接口
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤0 选择数据库类型(mysql/postgresql/oracle/sqlserver/dameng/kingbase/sqlite/mariadb)
|
||||||
|
↓
|
||||||
|
步骤1 配置连接信息(host/port/库名/用户名/密码/认证方式)
|
||||||
|
→ 点「下一步」自动触发 testConnection() 测试连接
|
||||||
|
→ 测试通过 → postConnectionDetail() / putConnectionDetail() 创建/更新数据源
|
||||||
|
→ emit('refresh') 通知列表刷新
|
||||||
|
→ getConnectionRealtimeStructure() 拉取真实库表结构
|
||||||
|
↓
|
||||||
|
步骤2 选择数据库和数据表(大数据量:shallowRef + 分批渲染 + 防抖搜索)
|
||||||
|
↓
|
||||||
|
步骤3 确认信息
|
||||||
|
→ postConnectionConfig() / putConnectionConfig() 落库「库表关联」配置
|
||||||
|
→ emit('success')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2.2 接口清单
|
||||||
|
|
||||||
|
| 功能 | 接口函数 | 方法 | 路径 | 关键入参 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 测试连接 | `testConnection(data)` | POST | `/api/datasource/connection/test` | host/port/databaseName/datasourceType/username/password/connectionType |
|
||||||
|
| 创建数据源 | `postConnectionDetail(data)` | POST | `/api/datasource/connection` | 同上 + datasourceName/remark |
|
||||||
|
| 更新数据源 | `putConnectionDetail(data)` | PUT | `/api/datasource/connection` | 同上 + id(编辑时密码可空表示不改) |
|
||||||
|
| 拉取实时库表结构 | `getConnectionRealtimeStructure(id)` | GET | `/api/datasource/connection/realtime/structure/{id}` | 数据源 id |
|
||||||
|
| 创建库表关联配置 | `postConnectionConfig(data)` | POST | `/api/datasource/config` | connectionId / syncTables / databases[] |
|
||||||
|
| 更新库表关联配置 | `putConnectionConfig(data)` | PUT | `/api/datasource/config` | id + 同上 |
|
||||||
|
| 删除配置 | `deleteConnectionConfig(ids[])` | POST | `/api/datasource/config/deletes` | `{ ids: [] }` |
|
||||||
|
|
||||||
|
- `postConnectionConfig` 请求体结构:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "配置ID(编辑传)",
|
||||||
|
"connectionId": "数据源ID",
|
||||||
|
"syncTables": true,
|
||||||
|
"databases": [
|
||||||
|
{ "databaseName": "库名", "tableNames": ["表1", "表2"] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
- 编辑模式不回显密码;密码留空 = 不修改密码。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 内置数据源(builtin / 内置 PostgreSQL)的创建与编辑
|
||||||
|
|
||||||
|
入口组件:[CreateBuiltinDataSource.vue](../src/components/databasePage/CreateBuiltinDataSource.vue)
|
||||||
|
支持三种模式:`create`(新建数据源)/ `edit`(编辑数据源)/ `addTable`(为已有库新增表)。
|
||||||
|
|
||||||
|
### 3.1 步骤与接口(create 模式)
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤0 填写数据源信息(datasourceName / remark)
|
||||||
|
→ postCreateBuiltinPostgreSQLConnection() 创建内置 PG 连接,返回 connectionId
|
||||||
|
↓
|
||||||
|
步骤1 填写业务场景描述 + 数据库名(仅字母数字下划线,自动加 _数据源id 后缀)
|
||||||
|
→ AI 生成表结构:postGenerateTable() 返回 taskId
|
||||||
|
→ 轮询 getAiTrainingDetail(taskId)(trainingStatus: 0待训/1训练中/2完成/3失败,最多 60 次 × 2s)
|
||||||
|
→ 解析 createTableData.data.tables 填入表列表
|
||||||
|
→ postCreateDatabase(connectionId, { databaseName }) 创建数据库
|
||||||
|
↓
|
||||||
|
步骤2 预览 / 编辑表结构(TableDetailEditor)
|
||||||
|
→ handleSubmit() 遍历表逐张 postCreateTable(connectionId, requestData)
|
||||||
|
→ emit('refresh') + emit('success')
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3.2 接口清单
|
||||||
|
|
||||||
|
| 功能 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 创建内置 PG 连接 | `postCreateBuiltinPostgreSQLConnection(data)` | POST | `/api/datasource/connection/create_builtin_postgresql` |
|
||||||
|
| 修改内置 PG 连接 | `putUpdateBuiltinDatabase(data)` | PUT | `/api/datasource/connection/update_builtin_database` |
|
||||||
|
| AI 生成表结构 | `postGenerateTable(data)` | POST | `/api/datasource/connection/generate_table` |
|
||||||
|
| 查询 AI 训练任务详情(轮询) | `getAiTrainingDetail(taskId)` | GET | `/api/ai/training/{taskId}` |
|
||||||
|
| 创建数据库 | `postCreateDatabase(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_database` |
|
||||||
|
| 创建数据表 | `postCreateTable(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_table` |
|
||||||
|
| 创建库+表(合并) | `postCreateDatabaseTable(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_database_table` |
|
||||||
|
|
||||||
|
- `addTable` 模式不直接调建表接口,而是 `emit('submitTables', tables)` 交回父组件(如 DatabaseDetail)提交。
|
||||||
|
- AI 业务描述润色走 `getAgentGeneric` + `AGENT_GENERIC_CODES.SQL_SCENARIO_DESCRIPTION`(`server/employee`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 数据库 / 表结构的增删改查(主要面向内置源)
|
||||||
|
|
||||||
|
入口组件:[DatabaseDetail.vue](../src/components/databasePage/DatabaseDetail.vue)(左侧表列表 + 右侧四视图)
|
||||||
|
|
||||||
|
### 4.1 表 / 字段的查询
|
||||||
|
|
||||||
|
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 表元数据列表(分页/无限滚动) | `getTablesList()` | `getTableAiList(params)` | GET | `/api/datasource/table/ailist` |
|
||||||
|
| 表详情(字段列表) | `executeSelectTable()` | `getTableDetail(id)` | GET | `/api/datasource/table/{id}/detail` |
|
||||||
|
| 表列表(基础) | — | `getTableList(data)` | GET | `/api/datasource/table/list` |
|
||||||
|
|
||||||
|
- `getTableAiList` 参数:`{ datasourceId, pageNum, pageSize }`(这里 `datasourceId` 实为「配置/库」id)。
|
||||||
|
- 切表有 Race Condition 防护:对比 `currentTableId` 拦截过期的详情请求。
|
||||||
|
|
||||||
|
### 4.2 库 / 表的增删改
|
||||||
|
|
||||||
|
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 修改数据库(改名) | `handleDatabaseSettingsConfirm()` | `putAlterDatabase(connectionId, data)` | PUT | `/api/datasource/connection/{connectionId}/alter_database` |
|
||||||
|
| 删除库(配置) | `handleDeleteDatabase()` | `deleteConnectionConfig(ids[])` | POST | `/api/datasource/config/deletes` |
|
||||||
|
| AI 智能建表(入口) | `createNewTable()` | 仅打开弹窗(内嵌 `CreateBuiltinDataSource` 的 addTable 模式) | — | — |
|
||||||
|
| 批量建表(提交) | `handleSubmitTables(tables)` | 循环 `postCreateTable(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_table` |
|
||||||
|
| 修改表结构 | `handleSaveTable()` | `putAlterTable(connectionId, data)` | PUT | `/api/datasource/connection/{connectionId}/alter_table` |
|
||||||
|
|
||||||
|
- 修改库后会重新 `getConnectionDetail(id)` 并 `emit('refresh', updatedDataSource)` 同步父级。
|
||||||
|
- 改表用增量 operation 标记字段:`ADD_COLUMN` / `MODIFY_COLUMN` / `DROP_COLUMN`(来自 TableDetailEditor)。
|
||||||
|
|
||||||
|
### 4.3 智能导入建表(Excel)
|
||||||
|
|
||||||
|
入口组件:[TableRecognition.vue](../src/components/databasePage/TableRecognition.vue)(两步向导)
|
||||||
|
|
||||||
|
```
|
||||||
|
步骤0 上传 Excel(≤500KB)+ 选环境
|
||||||
|
→ postImportDocumentPreview(connectionId, file, target) AI 识别前10条 → 表结构预览
|
||||||
|
↓
|
||||||
|
步骤1 编辑确认表结构与数据
|
||||||
|
→ postImportDocumentConfirm(connectionId, data, {target}) 建表 + 插入数据
|
||||||
|
→ emit('complete')
|
||||||
|
```
|
||||||
|
|
||||||
|
| 功能 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 文档导入预览(AI识别) | `postImportDocumentPreview(connectionId, file, target)` | POST | `/api/datasource/connection/{connectionId}/import_document/preview` |
|
||||||
|
| 文档导入确认(建表+插数据) | `postImportDocumentConfirm(connectionId, data, params)` | POST | `/api/datasource/connection/{connectionId}/import_document/confirm` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 内置表「数据行」的增删改查(builtin 专属,prod/test 双环境)
|
||||||
|
|
||||||
|
入口组件:[CustomizeDbTable.vue](../src/components/databasePage/CustomizeDbTable.vue)(包裹通用 `CommonDbTable`)
|
||||||
|
所有接口都带 `target` 参数区分线上 / 调试环境。
|
||||||
|
|
||||||
|
| 功能 | 组件回调 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 查(分页拉数据) | `onFetch` | `getBuiltinTableData(tableId, {pageNum,pageSize,target})` | GET | `/api/datasource/connection/builtin/table/{tableId}` |
|
||||||
|
| 增(新增行) | `onAdd` | `postBuiltinTableRows(tableId, data, {target})` | POST | `/api/datasource/connection/builtin/table/{tableId}/rows` |
|
||||||
|
| 改(更新行) | `onEdit` | `putBuiltinTableRows(tableId, data, {target})` | PUT | `/api/datasource/connection/builtin/table/{tableId}/rows` |
|
||||||
|
| 删(删除行) | `onDelete` | `deleteBuiltinTableRows(tableId, {primaryKeys}, {target})` | DELETE | `/api/datasource/connection/builtin/table/{tableId}/rows` |
|
||||||
|
| 导出 Excel | `onExport` | `getBuiltinTableExportExcel(tableId, {target})` | GET | `/api/datasource/connection/builtin/table/{tableId}/export/excel` |
|
||||||
|
| 导入数据 | `onImport` → `handleImportComplete` | `postImportDocumentConfirm(...)` | POST | 见上 |
|
||||||
|
|
||||||
|
- `onFetch` 把后端二维数组 `content` 按列名映射成对象数组。
|
||||||
|
- `onEdit/onDelete` 用动态主键(`rowKey`,默认 `id`)拼装 `primaryKey` / `primaryKeys`。
|
||||||
|
- 删除入参结构:`{ primaryKeys: [ { 主键字段: 值 }, ... ] }`。
|
||||||
|
- 导出接口为 `responseType: 'blob'`,含完整错误兜底(blob 是 JSON 时解析出 msg)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 智能问数:技能(skill)与工具(tool)的增删改查
|
||||||
|
|
||||||
|
入口组件:[ChatDebugging.vue](../src/components/databasePage/ChatDebugging.vue) + [SqlControllerMsg.vue](../src/components/databasePage/SqlControllerMsg.vue)
|
||||||
|
|
||||||
|
### 6.1 接口清单
|
||||||
|
|
||||||
|
| 功能 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 查询数据源配置(含 skillBool) | `getConnectionConfig(id)` | GET | `/api/datasource/config/{id}` |
|
||||||
|
| 按数据源查技能 | `getSkillByDatasource(id)` | GET | `/api/datasource/skill/getByDatasource/{id}` |
|
||||||
|
| 按技能 ID 查工具列表 | `getSkillBySkillId(id)` | GET | `/api/datasource/skill/getBySkillId/{id}` |
|
||||||
|
| 创建技能 | `postSkillCreateOrGet(data)` | POST | `/api/datasource/skill/createOrGet` |
|
||||||
|
| 修改技能 | `putSkillUpdateOrGet(data)` | POST | `/api/datasource/skill/updateOrGet` |
|
||||||
|
| 确认/创建技能工具 | `postSqlSkillConfirmTools(data)` | POST | `/api/datasource/skill/confirmTools` |
|
||||||
|
| 修改工具名/描述 | `postSkillToolUpdateOrGet(data)` | POST | `/api/datasource/skill/tskilltool/updateOrGet` |
|
||||||
|
| 删除技能工具 | `postDeleteSkillTool(skillToolId)` | DELETE | `/api/datasource/skill/tskilltool/{skillToolId}` |
|
||||||
|
|
||||||
|
> 说明:`executeSql`(POST `/api/datasource/sqlExecutionLog/testSqlWithSchema`)虽在 `server/database.ts` 中定义,但在 databasePage 模块内**未被任何组件调用**——它仅在工作流编辑器 [workflowPage/EditorMain.vue](../src/components/workflowPage/EditorMain.vue) 里使用。智能问数场景下,SQL 由后端 AI 通过 SSE 流执行并直接返回结果,前端不再单独调用执行接口。
|
||||||
|
|
||||||
|
### 6.2 「把查询沉淀为工具」业务链路(SqlControllerMsg 核心)
|
||||||
|
|
||||||
|
```
|
||||||
|
AI 返回 msgType=5 的 SQL 结果 → SqlControllerMsg 解析多视图
|
||||||
|
用户点「添加到工具」
|
||||||
|
→ 校验环境(test 禁止)、是否选库、重复检测
|
||||||
|
→ 分两条路径:
|
||||||
|
┌ skillBool=true(技能已存在)
|
||||||
|
│ → postSqlSkillConfirmTools() 直接确认工具
|
||||||
|
└ skillBool=false(技能不存在)
|
||||||
|
→ postSkillCreateOrGet() 创建技能
|
||||||
|
→ putSkillUpdateOrGet() 写入 MCP 配置模板 lzwcai-mcp-sqlexecutor
|
||||||
|
→ postSqlSkillConfirmTools() 确认工具
|
||||||
|
→ eventBus.emit(RELOAD_SKILL_DATA, datasourceId)
|
||||||
|
→ ChatDebugging 监听到事件 → loadSkillData() 刷新左侧技能/工具列表
|
||||||
|
```
|
||||||
|
|
||||||
|
- 重复检测链路:`getConnectionConfig → getSkillByDatasource → getSkillBySkillId`,比对 `sqlTemplate`。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. AI 补全训练
|
||||||
|
|
||||||
|
入口:[DatabaseDetail.vue](../src/components/databasePage/DatabaseDetail.vue)「AI补全管理」视图
|
||||||
|
|
||||||
|
| 功能 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 训练任务列表 | `getAiTrainingList(data)` | GET | `/api/ai/training/list` |
|
||||||
|
| 按选中表创建训练 | `postAiTrainingCreateBySelected(data)` | POST | `/api/ai/training/createBySelected` |
|
||||||
|
| 训练任务详情 | `getAiTrainingDetail(id)` | GET | `/api/ai/training/{id}` |
|
||||||
|
|
||||||
|
- 训练状态:`0` 待训练 / `1` 训练中 / `2` 已完成 / `3` 失败。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 字段关联同步(MQTT,builtin 专属)
|
||||||
|
|
||||||
|
入口:[DatabaseDetail.vue](../src/components/databasePage/DatabaseDetail.vue)「字段关联管理」视图
|
||||||
|
|
||||||
|
| 功能 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 同步配置列表 | `getMqttConfigList(params)` | GET | `/api/system/mqtt/config/list` |
|
||||||
|
| 配置详情 | `getMqttConfigDetail(configId)` | GET | `/api/system/mqtt/config/{configId}` |
|
||||||
|
| 目标表列表 | `getMqttConfigTableList()` | GET | `/api/system/mqtt/config/tableList` |
|
||||||
|
| 目标表字段列表 | `getMqttConfigTableColumns(tableName)` | GET | `/api/system/mqtt/config/tableColumns/{tableName}` |
|
||||||
|
| 新增配置 | `postMqttConfig(data)` | POST | `/api/system/mqtt/config` |
|
||||||
|
| 修改配置 | `putMqttConfig(data)` | PUT | `/api/system/mqtt/config` |
|
||||||
|
| 删除配置 | `deleteMqttConfig(ids)` | DELETE | `/api/system/mqtt/config/{ids}` |
|
||||||
|
| 刷新缓存 | `getMqttConfigRefreshCache()` | GET | `/api/system/mqtt/config/refreshCache` |
|
||||||
|
| 切换表订阅同步 | `postDatasourceSubscriptionToggle(data)` | POST | `/api/datasource/subscription/toggle` |
|
||||||
|
|
||||||
|
> ⚠️ 注意:提交配置时前端故意调换 `sourceTable` / `targetTable`,回显时再换回来(见 DatabaseDetail 的 `applyRelationConfigByTargetTable` 与 `loadRelationConfigByTableId`)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. API 密钥与权限的增删改查
|
||||||
|
|
||||||
|
入口组件:[DataSourceKeys.vue](../src/components/databasePage/DataSourceKeys.vue) + [DataSourceKeySetting.vue](../src/components/databasePage/DataSourceKeySetting.vue)
|
||||||
|
|
||||||
|
### 9.1 密钥 CRUD
|
||||||
|
|
||||||
|
| 功能 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 密钥列表 | `getApiKeyList(data)` | GET | `/api/datasource/api_key/list` |
|
||||||
|
| 新增密钥 | `postApiKey(data)` | POST | `/api/datasource/api_key` |
|
||||||
|
| 修改密钥(含启停) | `putApiKey(data)` | PUT | `/api/datasource/api_key` |
|
||||||
|
| 删除密钥 | `deleteApiKey(ids)` | DELETE | `/api/datasource/api_key/{ids}` |
|
||||||
|
|
||||||
|
### 9.2 权限配置
|
||||||
|
|
||||||
|
| 功能 | 接口函数 | 方法 | 路径 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| 查询密钥权限 | `getApiKeyPermission(apiKeyId)` | GET | `/api/datasource/api_key/permission/{apiKeyId}` |
|
||||||
|
| 批量授予权限 | `postApiKeyPermissionGrantBatch(data)` | POST | `/api/datasource/api_key/permission/grant_batch` |
|
||||||
|
|
||||||
|
权限配置走 `DataSourceKeySetting` 四步向导(数据源 → 数据库 → 数据表),分三级权限(connection / database / table)。其中加载列表复用:
|
||||||
|
|
||||||
|
| 步骤 | 接口函数 | 路径 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 选数据源 | `getConnectionList` | `/api/datasource/connection/list` |
|
||||||
|
| 选数据库 | `getConnectionConfigList` | `/api/datasource/config/list` |
|
||||||
|
| 选数据表 | `getTableList` | `/api/datasource/table/list` |
|
||||||
|
|
||||||
|
`postApiKeyPermissionGrantBatch` 请求体(batchDatas)每项:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"apiKeyId": "密钥ID",
|
||||||
|
"batchDatas": [
|
||||||
|
{
|
||||||
|
"connectionId": "数据源ID",
|
||||||
|
"permissionLevel": "connection|database|table",
|
||||||
|
"permissionType": "read,write(逗号拼接)",
|
||||||
|
"databaseName": "库名(database/table 级)",
|
||||||
|
"tableName": "表名(table 级)"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 端到端业务链路总览
|
||||||
|
|
||||||
|
### 链路 A:外置数据源「从创建到智能问数」
|
||||||
|
```
|
||||||
|
DataSourcePageMain → 添加远程数据源
|
||||||
|
→ CreateDataSource 4步:
|
||||||
|
testConnection → postConnectionDetail → getConnectionRealtimeStructure → postConnectionConfig
|
||||||
|
→ 列表出现卡片 → 点「业务执行」
|
||||||
|
→ ChatDebugging(员工1001)三栏 → DataSourcePlugIn 选库 → ChatBusiness 提问
|
||||||
|
→ 后端 AI 经 SSE 流执行 SQL 并返回结果 (msgType=5) → SqlControllerMsg 多视图渲染
|
||||||
|
→ 可「添加到工具」沉淀为技能
|
||||||
|
```
|
||||||
|
|
||||||
|
### 链路 B:内置数据源「从建库建表到数据管理」
|
||||||
|
```
|
||||||
|
DataSourcePageMain → 添加内置数据源
|
||||||
|
→ CreateBuiltinDataSource:
|
||||||
|
postCreateBuiltinPostgreSQLConnection
|
||||||
|
→ postGenerateTable + 轮询 getAiTrainingDetail(AI 建表结构)
|
||||||
|
→ postCreateDatabase → postCreateTable
|
||||||
|
→ DatabaseDetail 管理:
|
||||||
|
getTableAiList(表列表)→ getTableDetail(字段)
|
||||||
|
→ CustomizeDbTable 行级 CRUD(prod/test)
|
||||||
|
→ 智能导入 TableRecognition(preview→confirm)
|
||||||
|
→ 字段关联 MQTT / AI 补全训练 / 订阅
|
||||||
|
→ 业务执行(员工1002)智能问数同链路 A
|
||||||
|
```
|
||||||
|
|
||||||
|
### 链路 C:对外开放(API 密钥)
|
||||||
|
```
|
||||||
|
DataSourceKeys → postApiKey 建密钥
|
||||||
|
→ DataSourceKeySetting 配权限(连接→库→表三级)
|
||||||
|
→ postApiKeyPermissionGrantBatch 批量授权
|
||||||
|
→ ApiCallDocument 查看调用文档(/API_DOCUMENTATION.md)
|
||||||
|
→ 外部系统凭密钥调用数据查询
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 附录:完整接口索引(按 server/database.ts 顺序)
|
||||||
|
|
||||||
|
| # | 函数 | 方法 | 路径 | 用途 |
|
||||||
|
| --- | --- | --- | --- | --- |
|
||||||
|
| 1 | `getTableDetail` | GET | `/api/datasource/table/{id}/detail` | 表详情 |
|
||||||
|
| 2 | `getConnectionList` | GET | `/api/datasource/connection/list` | 连接列表 |
|
||||||
|
| 3 | `getConnectionDetail` | GET | `/api/datasource/connection/{id}` | 连接详情 |
|
||||||
|
| 4 | `getConnectionInstanceDetail` | GET | `/api/datasource/connection/{id}` | 连接实例详情 |
|
||||||
|
| 5 | `getTableList` | GET | `/api/datasource/table/list` | 表列表 |
|
||||||
|
| 6 | `getMqttConfigList` | GET | `/api/system/mqtt/config/list` | MQTT配置列表 |
|
||||||
|
| 7 | `getMqttConfigDetail` | GET | `/api/system/mqtt/config/{configId}` | MQTT配置详情 |
|
||||||
|
| 8 | `getMqttConfigTableList` | GET | `/api/system/mqtt/config/tableList` | MQTT目标表 |
|
||||||
|
| 9 | `getMqttConfigTableColumns` | GET | `/api/system/mqtt/config/tableColumns/{tableName}` | MQTT表字段 |
|
||||||
|
| 10 | `postMqttConfig` | POST | `/api/system/mqtt/config` | 新增MQTT配置 |
|
||||||
|
| 11 | `putMqttConfig` | PUT | `/api/system/mqtt/config` | 改MQTT配置 |
|
||||||
|
| 12 | `deleteMqttConfig` | DELETE | `/api/system/mqtt/config/{ids}` | 删MQTT配置 |
|
||||||
|
| 13 | `getMqttConfigRefreshCache` | GET | `/api/system/mqtt/config/refreshCache` | 刷新缓存 |
|
||||||
|
| 14 | `getTableAiList` | GET | `/api/datasource/table/ailist` | 表元数据列表 |
|
||||||
|
| 15 | `testConnection` | POST | `/api/datasource/connection/test` | 测试连接 |
|
||||||
|
| 16 | `postConnectionDetail` | POST | `/api/datasource/connection` | 创建连接 |
|
||||||
|
| 17 | `postCreateBuiltinPostgreSQLConnection` | POST | `/api/datasource/connection/create_builtin_postgresql` | 创建内置PG |
|
||||||
|
| 18 | `postCreateDatabaseTable` | POST | `/api/datasource/connection/{id}/create_database_table` | 创建库+表 |
|
||||||
|
| 19 | `postCreateTable` | POST | `/api/datasource/connection/{id}/create_table` | 创建表 |
|
||||||
|
| 20 | `postCreateDatabase` | POST | `/api/datasource/connection/{id}/create_database` | 创建库 |
|
||||||
|
| 21 | `putAlterDatabase` | PUT | `/api/datasource/connection/{id}/alter_database` | 改库 |
|
||||||
|
| 22 | `putAlterTable` | PUT | `/api/datasource/connection/{id}/alter_table` | 改表 |
|
||||||
|
| 23 | `postGenerateTable` | POST | `/api/datasource/connection/generate_table` | AI生成表结构 |
|
||||||
|
| 24 | `putUpdateBuiltinDatabase` | PUT | `/api/datasource/connection/update_builtin_database` | 改内置PG |
|
||||||
|
| 25 | `putConnectionDetail` | PUT | `/api/datasource/connection` | 更新连接 |
|
||||||
|
| 26 | `deleteConnection` | DELETE | `/api/datasource/connection/{id}` | 删连接 |
|
||||||
|
| 27 | `deleteConnectionConfig` | POST | `/api/datasource/config/deletes` | 删配置 |
|
||||||
|
| 28 | `putConnectionConfig` | PUT | `/api/datasource/config` | 改库表配置 |
|
||||||
|
| 29 | `postConnectionConfig` | POST | `/api/datasource/config` | 建库表配置 |
|
||||||
|
| 30 | `getConnectionRealtimeStructure` | GET | `/api/datasource/connection/realtime/structure/{id}` | 实时库表结构 |
|
||||||
|
| 31 | `getSkillByDatasource` | GET | `/api/datasource/skill/getByDatasource/{id}` | 按源查技能 |
|
||||||
|
| 32 | `getSkillBySkillId` | GET | `/api/datasource/skill/getBySkillId/{id}` | 查工具列表 |
|
||||||
|
| 33 | `postSkillCreateOrGet` | POST | `/api/datasource/skill/createOrGet` | 建技能 |
|
||||||
|
| 34 | `putSkillUpdateOrGet` | POST | `/api/datasource/skill/updateOrGet` | 改技能 |
|
||||||
|
| 35 | `postDeleteSkillTool` | DELETE | `/api/datasource/skill/tskilltool/{id}` | 删工具 |
|
||||||
|
| 36 | `postSkillToolUpdateOrGet` | POST | `/api/datasource/skill/tskilltool/updateOrGet` | 改工具 |
|
||||||
|
| 37 | `getAiTrainingList` | GET | `/api/ai/training/list` | 训练列表 |
|
||||||
|
| 38 | `postAiTrainingCreateBySelected` | POST | `/api/ai/training/createBySelected` | 建训练 |
|
||||||
|
| 39 | `getAiTrainingDetail` | GET | `/api/ai/training/{id}` | 训练详情 |
|
||||||
|
| 40 | `putConnectionChangeStatus` | PUT | `/api/datasource/connection/changeStatus` | 启停连接 |
|
||||||
|
| 41 | `getConnectionConfig` | GET | `/api/datasource/config/{id}` | 查源配置 |
|
||||||
|
| 42 | `getConnectionConfigList` | GET | `/api/datasource/config/list` | 配置列表 |
|
||||||
|
| 43 | `postSqlSkillConfirmTools` | POST | `/api/datasource/skill/confirmTools` | 确认工具 |
|
||||||
|
| 44 | `getApiKeyList` | GET | `/api/datasource/api_key/list` | 密钥列表 |
|
||||||
|
| 45 | `postApiKey` | POST | `/api/datasource/api_key` | 建密钥 |
|
||||||
|
| 46 | `putApiKey` | PUT | `/api/datasource/api_key` | 改密钥 |
|
||||||
|
| 47 | `deleteApiKey` | DELETE | `/api/datasource/api_key/{ids}` | 删密钥 |
|
||||||
|
| 48 | `getApiKeyPermission` | GET | `/api/datasource/api_key/permission/{apiKeyId}` | 查权限 |
|
||||||
|
| 49 | `postApiKeyPermissionGrantBatch` | POST | `/api/datasource/api_key/permission/grant_batch` | 批量授权 |
|
||||||
|
| 50 | `postDatasourceSubscriptionToggle` | POST | `/api/datasource/subscription/toggle` | 订阅开关 |
|
||||||
|
| 51 | `getBuiltinTableData` | GET | `/api/datasource/connection/builtin/table/{tableId}` | 查内置表数据 |
|
||||||
|
| 52 | `postBuiltinTableRows` | POST | `/api/datasource/connection/builtin/table/{tableId}/rows` | 增行 |
|
||||||
|
| 53 | `putBuiltinTableRows` | PUT | `/api/datasource/connection/builtin/table/{tableId}/rows` | 改行 |
|
||||||
|
| 54 | `deleteBuiltinTableRows` | DELETE | `/api/datasource/connection/builtin/table/{tableId}/rows` | 删行 |
|
||||||
|
| 55 | `getBuiltinTableExportExcel` | GET | `/api/datasource/connection/builtin/table/{tableId}/export/excel` | 导出Excel |
|
||||||
|
| 56 | `postImportDocumentPreview` | POST | `/api/datasource/connection/{id}/import_document/preview` | 导入预览 |
|
||||||
|
| 57 | `postImportDocumentConfirm` | POST | `/api/datasource/connection/{id}/import_document/confirm` | 导入确认 |
|
||||||
|
| 58 | `executeSql` | POST | `/api/datasource/sqlExecutionLog/testSqlWithSchema` | 执行SQL |
|
||||||
623
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-MCP工具设计方案.md
Normal file
623
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-MCP工具设计方案.md
Normal file
@@ -0,0 +1,623 @@
|
|||||||
|
# 数据库管理平台 - MCP 工具设计方案
|
||||||
|
|
||||||
|
> 基于平台现有功能,设计可供外部 AI Agent 通过 MCP 协议调用的工具集。
|
||||||
|
> 目标:**用户脱离平台界面,通过 MCP 调用即可使用数据库管理平台的核心功能。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、设计原则
|
||||||
|
|
||||||
|
1. **按用户场景分组**:不是简单映射 API,而是围绕用户真实工作流组织工具
|
||||||
|
2. **最小化调用链**:复杂操作尽量合并为一个 tool,减少多轮调用
|
||||||
|
3. **读写分离**:查询类工具可安全暴露,写操作需明确提示
|
||||||
|
4. **环境感知**:所有操作默认携带环境参数(prod/test),内置数据源特有
|
||||||
|
5. **权限前置**:调用方需提供 apiKeyId 或等效凭证,工具内部自动鉴权
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、工具清单
|
||||||
|
|
||||||
|
### 🗄️ 数据源管理
|
||||||
|
|
||||||
|
#### 1. `list_datasources`
|
||||||
|
- **用途**:获取数据源列表,支持搜索和状态筛选
|
||||||
|
- **对应前端**:DataSourceList.vue
|
||||||
|
- **对应 API**:`getConnectionList` ✅ 已实现
|
||||||
|
- **端点**:`GET /api/datasource/connection/list`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| datasourceName | string | 否 | 数据源名称模糊搜索 |
|
||||||
|
| status | int | 否 | 0=运行中, 1=已停止, 不传=全部 |
|
||||||
|
| sourceType | string | 否 | builtin/external |
|
||||||
|
| pageNum | int | 否 | 默认 1 |
|
||||||
|
| pageSize | int | 否 | 默认 20 |
|
||||||
|
|
||||||
|
**返回**:数据源列表(含名称、类型、状态、库数、表数、创建时间等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 2. `get_datasource_detail`
|
||||||
|
- **用途**:获取单个数据源的完整详情
|
||||||
|
- **对应前端**:DatabaseDetail.vue 头部
|
||||||
|
- **对应 API**:
|
||||||
|
- `getConnectionDetail(id)` ✅ 已实现 — `GET /api/datasource/connection/{id}`
|
||||||
|
- `getConnectionConfig(id)` ✅ 已实现 — `GET /api/datasource/config/{id}`
|
||||||
|
- `getConnectionRealtimeStructure(id)` ✅ 已实现 — `GET /api/datasource/connection/realtime/structure/{id}`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| datasourceId | string | 是 | 数据源 ID |
|
||||||
|
|
||||||
|
**返回**:数据源详情 + 数据库配置列表 + 实时结构(数据库列表、表列表、字段信息)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 3. `create_datasource`
|
||||||
|
- **用途**:创建外部数据源连接
|
||||||
|
- **对应前端**:CreateDataSource.vue
|
||||||
|
- **对应 API**:
|
||||||
|
- `testConnection(data)` ✅ 已实现 — `POST /api/datasource/connection/test`
|
||||||
|
- `postConnectionDetail(data)` ✅ 已实现 — `POST /api/datasource/connection`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| datasourceName | string | 是 | 数据源名称(3-20字) |
|
||||||
|
| datasourceType | string | 是 | mysql/postgresql/oracle/sqlserver/dameng |
|
||||||
|
| host | string | 是 | 数据库地址 |
|
||||||
|
| port | int | 是 | 端口号 |
|
||||||
|
| databaseName | string | 是 | 要连接的数据库名 |
|
||||||
|
| username | string | 是 | 数据库用户名 |
|
||||||
|
| password | string | 否 | 密码 |
|
||||||
|
| remark | string | 否 | 数据源描述 |
|
||||||
|
| connectionType | string | 否 | user_password/ssl,默认 user_password |
|
||||||
|
| test_first | bool | 否 | 是否先测试连接,默认 true |
|
||||||
|
|
||||||
|
**返回**:创建结果,含数据源 ID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 4. `update_datasource`
|
||||||
|
- **用途**:更新数据源连接信息
|
||||||
|
- **对应 API**:`putConnectionDetail(data)` ✅ 已实现 — `PUT /api/datasource/connection`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | string | 是 | 数据源 ID |
|
||||||
|
| datasourceName | string | 否 | 更新名称 |
|
||||||
|
| host | string | 否 | 更新地址 |
|
||||||
|
| port | int | 否 | 更新端口 |
|
||||||
|
| databaseName | string | 否 | 更新数据库名 |
|
||||||
|
| username | string | 否 | 更新用户名 |
|
||||||
|
| password | string | 否 | 新密码(不传则不变) |
|
||||||
|
| remark | string | 否 | 更新描述 |
|
||||||
|
|
||||||
|
**返回**:更新结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 5. `toggle_datasource_status`
|
||||||
|
- **用途**:启用/停用数据源
|
||||||
|
- **对应 API**:`putConnectionChangeStatus(data)` ✅ 已实现 — `PUT /api/datasource/connection/changeStatus`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | string | 是 | 数据源 ID |
|
||||||
|
| status | int | 是 | 0=启用, 1=停用 |
|
||||||
|
|
||||||
|
**返回**:操作结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 6. `delete_datasource`
|
||||||
|
- **用途**:删除数据源(运行中会自动先停用)
|
||||||
|
- **对应 API**:
|
||||||
|
- `putConnectionChangeStatus(data)` ✅ 已实现 — `PUT /api/datasource/connection/changeStatus`
|
||||||
|
- `deleteConnection(id)` ✅ 已实现 — `DELETE /api/datasource/connection/{id}`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | string | 是 | 数据源 ID |
|
||||||
|
|
||||||
|
**返回**:删除结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📊 数据库与表管理
|
||||||
|
|
||||||
|
#### 7. `list_databases`
|
||||||
|
- **用途**:获取指定数据源下的数据库列表
|
||||||
|
- **对应 API**:`getConnectionConfigList(data)` ✅ 已实现 — `GET /api/datasource/config/list`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| datasourceId | string | 是 | 数据源 ID |
|
||||||
|
|
||||||
|
**返回**:数据库列表(ID、名称、类型、状态、表数)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 8. `list_tables`
|
||||||
|
- **用途**:获取指定数据库下的表列表
|
||||||
|
- **对应前端**:DatabaseDetail.vue 左侧表列表
|
||||||
|
- **对应 API**:`getTableList(data)` ✅ 已实现 — `GET /api/datasource/table/list`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| datasourceId | string | 是 | 数据源 ID |
|
||||||
|
| databaseName | string | 否 | 数据库名过滤 |
|
||||||
|
| keyword | string | 否 | 表名模糊搜索 |
|
||||||
|
| pageNum | int | 否 | 默认 1 |
|
||||||
|
| pageSize | int | 否 | 默认 20 |
|
||||||
|
|
||||||
|
**返回**:表列表(表名、注释、字段数、创建时间等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 9. `get_table_detail`
|
||||||
|
- **用途**:获取表的完整结构信息
|
||||||
|
- **对应前端**:DatabaseDetail.vue 右侧字段列表
|
||||||
|
- **对应 API**:`getTableDetail(id)` ✅ 已实现 — `GET /api/datasource/table/{id}/detail`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| tableId | string | 是 | 表 ID |
|
||||||
|
|
||||||
|
**返回**:表名、表注释、字段列表(字段名、类型、长度、主键、可空、默认值、注释、AI训练状态)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 10. `create_table`
|
||||||
|
- **用途**:在指定数据库创建新表
|
||||||
|
- **对应前端**:CreateBuiltinDataSource.vue 表结构编辑器
|
||||||
|
- **对应 API**:`postCreateTable(connectionId, data)` ✅ 已实现 — `POST /api/datasource/connection/{connectionId}/create_table`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| connectionId | string | 是 | 数据源连接 ID(路径参数) |
|
||||||
|
| databaseName | string | 是 | 目标数据库名 |
|
||||||
|
| tableName | string | 是 | 表名(小写字母+数字+下划线) |
|
||||||
|
| tableComment | string | 否 | 表注释 |
|
||||||
|
| columns | array | 是 | 字段定义数组 |
|
||||||
|
| columns[].columnName | string | 是 | 字段名 |
|
||||||
|
| columns[].columnType | string | 是 | 字段类型(VARCHAR/INTEGER/SERIAL等) |
|
||||||
|
| columns[].columnLength | int | 否 | 字段长度 |
|
||||||
|
| columns[].isPrimaryKey | bool | 否 | 是否主键 |
|
||||||
|
| columns[].isNullable | bool | 否 | 是否可空 |
|
||||||
|
| columns[].isAutoIncrement | bool | 否 | 是否自增 |
|
||||||
|
| columns[].columnComment | string | 否 | 字段注释 |
|
||||||
|
| columns[].defaultValue | string | 否 | 默认值 |
|
||||||
|
|
||||||
|
**返回**:创建结果,含表 ID
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 11. `alter_table`
|
||||||
|
- **用途**:修改已有表结构(增/改/删字段)
|
||||||
|
- **对应 API**:`putAlterTable(connectionId, data)` ✅ 已实现 — `PUT /api/datasource/connection/{connectionId}/alter_table`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| connectionId | string | 是 | 数据源连接 ID(路径参数) |
|
||||||
|
| databaseName | string | 是 | 数据库名 |
|
||||||
|
| tableName | string | 是 | 表名 |
|
||||||
|
| operations | array | 是 | 表结构变更操作数组 |
|
||||||
|
| operations[].operation | string | 是 | ADD_COLUMN / DROP_COLUMN / RENAME_COLUMN / ALTER_COLUMN_TYPE / SET_NOT_NULL / DROP_NOT_NULL / SET_DEFAULT / DROP_DEFAULT |
|
||||||
|
| operations[].column | object | 是 | 列定义(根据 operation 不同包含不同字段,如 columnName/columnType/newColumnName 等) |
|
||||||
|
| tableComment | string | 否 | 新表注释 |
|
||||||
|
|
||||||
|
**返回**:修改结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 12. `generate_table_by_description`
|
||||||
|
- **用途**:通过自然语言描述让 AI 生成表结构(异步任务)
|
||||||
|
- **对应前端**:CreateBuiltinDataSource.vue AI 生成表结构
|
||||||
|
- **对应 API**:`postGenerateTable(data)` ✅ 已实现 — `POST /api/datasource/connection/generate_table`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| requirement | string | 是 | 业务需求描述 |
|
||||||
|
| databaseId | int | 否 | 关联的数据库 ID |
|
||||||
|
|
||||||
|
**返回**:异步任务信息(taskId、status),需轮询任务状态获取最终生成的表结构
|
||||||
|
|
||||||
|
> **场景示例**:用户说"我需要一个商城系统,管理商品、分类和用户评价",AI 返回完整的表结构设计。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📝 表数据操作 (内置数据源 CRUD)
|
||||||
|
|
||||||
|
#### 13. `query_table_data`
|
||||||
|
- **用途**:查询内置表数据(分页)
|
||||||
|
- **对应前端**:CustomizeDbTable.vue 线上/调试数据视图
|
||||||
|
- **对应 API**:`getBuiltinTableData(tableId, params)` ✅ 已实现 — `GET /api/datasource/connection/builtin/table/{tableId}`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| tableId | string | 是 | 表 ID |
|
||||||
|
| target | string | 否 | prod/test,默认 prod |
|
||||||
|
| pageNum | int | 否 | 默认 1 |
|
||||||
|
| pageSize | int | 否 | 默认 10 |
|
||||||
|
|
||||||
|
**返回**:表结构信息 + 数据行(二维数组转换后的对象数组) + 总数
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 14. `insert_table_row`
|
||||||
|
- **用途**:向内置表插入一行数据
|
||||||
|
- **对应 API**:`postBuiltinTableRows(tableId, data, params)` ✅ 已实现 — `POST /api/datasource/connection/builtin/table/{tableId}/rows`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| tableId | string | 是 | 表 ID |
|
||||||
|
| target | string | 否 | prod/test,默认 prod |
|
||||||
|
| data | object | 是 | 行数据(键值对,键为字段名) |
|
||||||
|
|
||||||
|
**返回**:插入结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 15. `update_table_row`
|
||||||
|
- **用途**:更新内置表的指定行
|
||||||
|
- **对应 API**:`putBuiltinTableRows(tableId, data, params)` ✅ 已实现 — `PUT /api/datasource/connection/builtin/table/{tableId}/rows`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| tableId | string | 是 | 表 ID |
|
||||||
|
| target | string | 否 | prod/test,默认 prod |
|
||||||
|
| primaryKey | object | 是 | 主键值(如 {"id": 1}) |
|
||||||
|
| data | object | 是 | 要更新的字段值 |
|
||||||
|
|
||||||
|
**返回**:更新结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 16. `delete_table_rows`
|
||||||
|
- **用途**:删除内置表的指定行
|
||||||
|
- **对应 API**:`deleteBuiltinTableRows(tableId, data, params)` ✅ 已实现 — `DELETE /api/datasource/connection/builtin/table/{tableId}/rows`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| tableId | string | 是 | 表 ID |
|
||||||
|
| target | string | 否 | prod/test,默认 prod |
|
||||||
|
| primaryKeys | array | 是 | 主键数组(如 [{"id": 1}, {"id": 2}]) |
|
||||||
|
|
||||||
|
**返回**:删除结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 17. `export_table_excel`
|
||||||
|
- **用途**:导出表数据为 Excel 文件
|
||||||
|
- **对应 API**:`getBuiltinTableExportExcel(tableId, params)` ✅ 已实现 — `GET /api/datasource/connection/builtin/table/{tableId}/export/excel`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| tableId | string | 是 | 表 ID |
|
||||||
|
| target | string | 否 | prod/test,默认 prod |
|
||||||
|
|
||||||
|
**返回**:Excel 文件(二进制 blob)+ 文件名(从 Content-Disposition 解析)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔑 API 密钥与权限管理
|
||||||
|
|
||||||
|
#### 18. `list_api_keys`
|
||||||
|
- **用途**:获取 API 密钥列表
|
||||||
|
- **对应前端**:DataSourceKeys.vue
|
||||||
|
- **对应 API**:`getApiKeyList(data)` ✅ 已实现 — `GET /api/datasource/api_key/list`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| apiKeyName | string | 否 | 密钥名称模糊搜索 |
|
||||||
|
| pageNum | int | 否 | 默认 1 |
|
||||||
|
| pageSize | int | 否 | 默认 20 |
|
||||||
|
|
||||||
|
**返回**:密钥列表(名称、Key、状态、创建时间)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 19. `create_api_key`
|
||||||
|
- **用途**:创建新的 API 密钥
|
||||||
|
- **对应 API**:`postApiKey(data)` ✅ 已实现 — `POST /api/datasource/api_key`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| apiKeyName | string | 是 | 密钥名称(最多50字) |
|
||||||
|
|
||||||
|
**返回**:密钥信息(含 API Key 明文、ID)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 20. `toggle_api_key_status`
|
||||||
|
- **用途**:启用/禁用 API 密钥
|
||||||
|
- **对应 API**:`putApiKey(data)` ✅ 已实现 — `PUT /api/datasource/api_key`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | string | 是 | 密钥 ID |
|
||||||
|
| status | int | 是 | 0=启用, 1=禁用 |
|
||||||
|
|
||||||
|
**返回**:操作结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 21. `delete_api_key`
|
||||||
|
- **用途**:删除 API 密钥
|
||||||
|
- **对应 API**:`deleteApiKey(ids)` ✅ 已实现 — `DELETE /api/datasource/api_key/{ids}`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| id | string | 是 | 密钥 ID |
|
||||||
|
|
||||||
|
**返回**:删除结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 22. `get_api_key_permissions`
|
||||||
|
- **用途**:查看指定密钥的权限配置
|
||||||
|
- **对应 API**:`getApiKeyPermission(apiKeyId)` ✅ 已实现 — `GET /api/datasource/api_key/permission/{apiKeyId}`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| apiKeyId | string | 是 | 密钥 ID |
|
||||||
|
|
||||||
|
**返回**:三级权限(connectionPermissions / databasePermissions / tablePermissions)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 23. `grant_api_key_permissions`
|
||||||
|
- **用途**:批量为 API 密钥授予权限
|
||||||
|
- **对应前端**:DataSourceKeySetting.vue
|
||||||
|
- **对应 API**:`postApiKeyPermissionGrantBatch(data)` ✅ 已实现 — `POST /api/datasource/api_key/permission/grant_batch`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| apiKeyId | string | 是 | 密钥 ID |
|
||||||
|
| batchDatas | array | 是 | 权限批量数据数组 |
|
||||||
|
| batchDatas[].connectionId | string | 是 | 数据源 ID |
|
||||||
|
| batchDatas[].permissionLevel | string | 是 | connection/database/table |
|
||||||
|
| batchDatas[].permissionType | string | 是 | 权限类型(逗号分隔) |
|
||||||
|
| batchDatas[].databaseName | string | 否 | 数据库名(level=database/table 时) |
|
||||||
|
| batchDatas[].tableName | string | 否 | 表名(level=table 时) |
|
||||||
|
|
||||||
|
**返回**:授权结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 23.5 ~~`revoke_api_key_permissions`~~(已废弃,不提供)
|
||||||
|
- **结论**:权限为「仅追加」模型。真机验证后端 permission 删除接口返回「不支持当前的调用方式」,无法撤销单条已授予权限,故不实现该工具。
|
||||||
|
- **替代方案**:要缩小某密钥的权限范围,只能「删密钥(`delete_api_key`)→ 重建(`create_api_key`)→ 重新授权(`grant_api_key_permissions`)」。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🤖 技能与工具管理 (内置数据源 AI 能力)
|
||||||
|
|
||||||
|
#### 24. `get_skill_by_datasource`
|
||||||
|
- **用途**:根据数据源获取技能信息
|
||||||
|
- **对应 API**:`getSkillByDatasource(id)` ✅ 已实现 — `GET /api/datasource/skill/getByDatasource/{id}`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| datasourceId | string | 是 | 数据源 ID |
|
||||||
|
|
||||||
|
**返回**:技能信息(含 skillBool 标识)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 25. `get_skill_tools`
|
||||||
|
- **用途**:获取技能下的工具列表
|
||||||
|
- **对应 API**:`getSkillBySkillId(id)` ✅ 已实现 — `GET /api/datasource/skill/getBySkillId/{id}`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| skillId | string | 是 | 技能 ID |
|
||||||
|
|
||||||
|
**返回**:工具列表(名称、描述、SQL模板、参数定义、业务场景)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 26. ~~`create_skill`~~(已移除,不再单独暴露)
|
||||||
|
- **结论**:技能(skill)必须挂着工具才有效,平时不会单独创建技能;单独建技能会留下无效的空技能。前端也没有「只建技能」入口。
|
||||||
|
- **替代方案**:把 SQL 沉淀为工具统一用 `add_sql_tool_to_datasource`(见 §29.5),它内部按需调 `skill/createOrGet` 建技能、写配置模板、再 `confirmTools` 建工具,一步到位且保证技能必有工具。底层 `skill/createOrGet` 端点仍被该编排工具内部使用,只是不再作为独立 MCP 工具暴露。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 27. `create_sql_tool`
|
||||||
|
- **用途**:将 SQL 查询创建为可复用工具
|
||||||
|
- **对应前端**:SqlControllerMsg.vue 添加到工具功能
|
||||||
|
- **对应 API**:`postSqlSkillConfirmTools(data)` ✅ 已实现 — `POST /api/datasource/skill/confirmTools`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| skillId | string | 是 | 技能 ID |
|
||||||
|
| tableIds | array | 否 | 关联的表 ID 数组 |
|
||||||
|
| suggestions | array | 是 | SQL 工具建议数组(支持批量) |
|
||||||
|
| suggestions[].name | string | 是 | 工具名称 |
|
||||||
|
| suggestions[].businessDescription | string | 是 | 业务描述 |
|
||||||
|
| suggestions[].sqlTemplate | string | 是 | SQL 模板(支持 #{param} 参数占位) |
|
||||||
|
| suggestions[].sqlParams | string/object | 否 | 参数 JSON Schema(对象会自动序列化为字符串) |
|
||||||
|
| suggestions[].resultType | string | 否 | single/list,默认 list |
|
||||||
|
| suggestions[].businessScenario | string | 否 | 业务场景描述 |
|
||||||
|
|
||||||
|
**返回**:工具创建结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 28. `delete_skill_tool`
|
||||||
|
- **用途**:删除技能下的工具
|
||||||
|
- **对应 API**:`postDeleteSkillTool(skillToolId)` ✅ 已实现 — `DELETE /api/datasource/skill/tskilltool/{skillToolId}`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| skillToolId | string | 是 | 工具 ID |
|
||||||
|
|
||||||
|
**返回**:删除结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 29. `update_skill_config`
|
||||||
|
- **用途**:更新技能配置(如 MCP Server 配置模板)
|
||||||
|
- **对应 API**:`putSkillUpdateOrGet(data)` ✅ 已实现 — `POST /api/datasource/skill/updateOrGet`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| datasourceId | string | 是 | 数据源 ID |
|
||||||
|
| configTemplate | string | 是 | 配置模板 JSON 字符串 |
|
||||||
|
|
||||||
|
**返回**:更新结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 📥 数据导入
|
||||||
|
|
||||||
|
#### 30. `preview_import_data`
|
||||||
|
- **用途**:上传 Excel 文件,AI 智能识别并预览表结构/数据
|
||||||
|
- **对应前端**:TableRecognition.vue
|
||||||
|
- **对应 API**:`postImportDocumentPreview(connectionId, file, target)` ✅ 已实现 — `POST /api/datasource/connection/{connectionId}/import_document/preview`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| connectionId | int | 是 | 数据源 ID |
|
||||||
|
| target | string | 否 | prod/test,默认 test |
|
||||||
|
| file | binary | 是 | Excel 文件 (.xlsx/.xls, <500KB) |
|
||||||
|
|
||||||
|
**返回**:识别结果(表结构 + 数据预览)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
#### 31. `confirm_import_data`
|
||||||
|
- **用途**:确认导入 AI 识别后的数据
|
||||||
|
- **对应 API**:`postImportDocumentConfirm(connectionId, data, params)` ✅ 已实现 — `POST /api/datasource/connection/{connectionId}/import_document/confirm`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| connectionId | int | 是 | 数据源 ID |
|
||||||
|
| databaseName | string | 是 | 落库目标库名 |
|
||||||
|
| target | string | 否 | prod/test |
|
||||||
|
| data | object | 是 | 导入数据(含 tableStructure + allData) |
|
||||||
|
|
||||||
|
**data.tableStructure.columns 的关键约束**:
|
||||||
|
- `columns` 定义了「这批数据要写入哪些字段」,**导入时这些列必须对应目标表中真实存在的字段**——列名(`columnName`)要与目标表的实际字段名一致,否则后端按列名拼 INSERT 时会报「查询字段不存在 / 字段名称不正确」。
|
||||||
|
- `allData` 的**首行是列名表头**(= `columns[].columnName`),数据从第 2 行起;每行按 `columns` 顺序给出**全部列**的值(全列宽,不裁剪)。表头列名同样必须是目标表存在的字段。
|
||||||
|
- **导入到已有表时**:前端会用目标表的真实列(`get_table_detail` 返回的 columns)覆盖 AI 识别的列。MCP 调用方应等价处理——**先 `get_table_detail` 拿到目标表真实字段,再让 data 里的 columns / 表头 / 数据与之对齐**,不要直接用 Excel 识别出的、可能与表字段不符的列。
|
||||||
|
|
||||||
|
**返回**:导入结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🏷️ 表订阅
|
||||||
|
|
||||||
|
#### 32. `toggle_table_subscription`
|
||||||
|
- **用途**:切换表的订阅状态
|
||||||
|
- **对应前端**:DatabaseDetail.vue 订阅按钮
|
||||||
|
- **对应 API**:`postDatasourceSubscriptionToggle(data)` ✅ 已实现 — `POST /api/datasource/subscription/toggle`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| tableId | string | 是 | 表 ID |
|
||||||
|
| datasourceId | string | 是 | 数据源 ID |
|
||||||
|
| subscribe | bool | 是 | true=订阅, false=取消订阅 |
|
||||||
|
|
||||||
|
**返回**:操作结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 🔧 SQL 执行
|
||||||
|
|
||||||
|
#### 33. `execute_sql`
|
||||||
|
- **用途**:执行原生 SQL 查询
|
||||||
|
- **对应 API**:`executeSql(data)` ✅ 已实现 — `POST /api/datasource/sqlExecutionLog/testSqlWithSchema`
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| datasourceId | string | 是 | 数据源 ID |
|
||||||
|
| sql | string | 是 | SQL 语句 |
|
||||||
|
| target | string | 否 | prod/test,默认 prod |
|
||||||
|
| params | object | 否 | 参数对象 |
|
||||||
|
|
||||||
|
**返回**:查询结果(表头、数据行、业务名称、描述等)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、推荐使用场景
|
||||||
|
|
||||||
|
### 场景 1:外部 AI Agent 管理数据库
|
||||||
|
|
||||||
|
```
|
||||||
|
用户: "帮我查一下有哪些数据源"
|
||||||
|
Agent: 调用 list_datasources()
|
||||||
|
|
||||||
|
用户: "看看 mall_db 有哪些表"
|
||||||
|
Agent: 调用 list_tables(datasourceId="xx", databaseName="mall_db")
|
||||||
|
|
||||||
|
用户: "users 表结构给我看一下"
|
||||||
|
Agent: 调用 get_table_detail(tableId="xx")
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 2:通过 AI 描述自动生成表结构
|
||||||
|
|
||||||
|
```
|
||||||
|
用户: "我需要一个订单系统,包含订单、订单明细、支付方式"
|
||||||
|
Agent: 调用 generate_table_by_description(description="我需要一个订单系统...")
|
||||||
|
Agent: 返回 AI 生成的表结构,用户确认后调用 create_table() 批量创建
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 3:管理 API 密钥和权限
|
||||||
|
|
||||||
|
```
|
||||||
|
用户: "帮我创建一个叫'第三方报表系统'的 API Key"
|
||||||
|
Agent: 调用 create_api_key(apiKeyName="第三方报表系统")
|
||||||
|
|
||||||
|
用户: "给它开通 mall_db 数据库的读取权限"
|
||||||
|
Agent: 调用 grant_api_key_permissions(apiKeyId="xx", batchDatas=[{...}])
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 4:管理表数据
|
||||||
|
|
||||||
|
```
|
||||||
|
用户: "查一下 users 表前 10 条数据"
|
||||||
|
Agent: 调用 query_table_data(tableId="xx", pageNum=1, pageSize=10)
|
||||||
|
|
||||||
|
用户: "新增一个用户,用户名是 test_user"
|
||||||
|
Agent: 调用 insert_table_row(tableId="xx", data={"user_name": "test_user", ...})
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 5:创建 SQL 工具
|
||||||
|
|
||||||
|
```
|
||||||
|
用户: "把这个查询保存为工具,叫'按地区统计订单'"
|
||||||
|
Agent: 调用 create_sql_tool(skillId="xx", name="按地区统计订单",
|
||||||
|
sqlTemplate="SELECT region, COUNT(*) FROM orders GROUP BY region", ...)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景 6:导入 Excel 数据
|
||||||
|
|
||||||
|
```
|
||||||
|
用户: "帮我导入这份 Excel 到测试环境"
|
||||||
|
Agent: 调用 preview_import_data(connectionId=xx, target="test", file=...)
|
||||||
|
Agent: 展示识别结果,用户确认后调用 confirm_import_data(...)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、工具优先级建议
|
||||||
|
|
||||||
|
| 优先级 | 工具 | 理由 |
|
||||||
|
|--------|------|------|
|
||||||
|
| **P0 核心** | list_datasources, list_tables, get_table_detail, execute_sql, query_table_data | 覆盖 80% 查询场景 |
|
||||||
|
| **P1 常用** | create_datasource, create_table, generate_table_by_description, create_api_key, grant_api_key_permissions | 管理核心操作 |
|
||||||
|
| **P2 扩展** | insert/update/delete_table_row, export_table_excel, create_sql_tool, toggle_table_subscription | 数据操作与 AI 能力 |
|
||||||
|
| **P3 完整** | preview/confirm_import_data, update_skill_config, alter_table, delete_skill_tool | 高级功能 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 五、实现建议
|
||||||
|
|
||||||
|
1. **MCP Server 实现**:建议用 Python + `mcp` 库或 Node.js + `@modelcontextprotocol/sdk`
|
||||||
|
2. **鉴权方式**:工具内部复用平台现有的 API Key 鉴权机制,调用方传入 apiKeyId
|
||||||
|
3. **错误处理**:统一错误格式 `{ success: false, error: "描述" }`,与平台 API 拦截器保持一致
|
||||||
|
4. **环境默认值**:所有 target 参数默认 prod,调用方显式指定 test 才能操作测试数据
|
||||||
|
5. **批量操作**:对于 create_table 等可能涉及多表的场景,支持批量参数或循环调用
|
||||||
|
6. **AI 工具调用**:generate_table_by_description 等 AI 工具需要调用平台的 AI 服务接口(agent generic / aiTextGenerator)
|
||||||
|
7. **前端 API 映射**:所有 33 个 MCP 工具均已在前端 `src/server/database.ts` 中实现对应函数,可直接复用
|
||||||
303
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-功能总览.md
Normal file
303
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-功能总览.md
Normal file
@@ -0,0 +1,303 @@
|
|||||||
|
# 数据库管理平台 - 功能总览
|
||||||
|
|
||||||
|
> 本文档梳理 `databasePage` 组件集群与 `DatabaseDetailPage` 的整体功能架构。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 一、系统定位
|
||||||
|
|
||||||
|
这是一个面向业务用户的**数据库管理与智能问数一体化平台**,支持两种数据源模式:
|
||||||
|
|
||||||
|
| 模式 | 说明 | 核心差异 |
|
||||||
|
|------|------|----------|
|
||||||
|
| **外部数据源** | 远程 MySQL/PostgreSQL/Oracle/SQL Server/达梦 等 | 标准数据库连接,提供只读/CRUD 管理 |
|
||||||
|
| **内置数据源** | 平台内建 PostgreSQL 实例 | 支持 AI 技能、工具、环境切换(prod/test)、AI 训练 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 二、功能模块全景
|
||||||
|
|
||||||
|
### 1. 数据源管理
|
||||||
|
|
||||||
|
#### 1.1 数据源列表 (`DataSourceList.vue`)
|
||||||
|
- **卡片式展示**:名称、ID、类型徽章(内置/外部)、数据库类型、运行状态、库数/表数统计
|
||||||
|
- **无限滚动分页**:IntersectionObserver + useInfiniteScroll,每页 20 条
|
||||||
|
- **搜索与筛选**:按名称模糊搜索、按状态筛选(运行中/已停止)
|
||||||
|
- **数据源操作**:
|
||||||
|
- 编辑(外部→CreateDataSource, 内置→CreateBuiltinDataSource)
|
||||||
|
- 启用/停用(切换 status 0/1)
|
||||||
|
- 删除(运行中自动先停用再删除)
|
||||||
|
- **业务执行入口**:点击打开智能问数聊天界面 (ChatDebugging)
|
||||||
|
- **查看详情入口**:右侧 Drawer 打开 DatabaseDetail (95% 宽度)
|
||||||
|
|
||||||
|
#### 1.2 外部数据源创建/编辑 (`CreateDataSource.vue`) - 4步向导
|
||||||
|
| 步骤 | 内容 | 核心功能 |
|
||||||
|
|------|------|----------|
|
||||||
|
| Step 1 | 选择数据库类型 | MySQL/PostgreSQL/Oracle/SQL Server/达梦,自动填充默认端口 |
|
||||||
|
| Step 2 | 配置连接信息 | 名称、描述(AI润色)、主机、端口、认证(基础认证/SSL)、用户名密码(AI生成名称)、测试连接 |
|
||||||
|
| Step 3 | 选择数据库和表 | 树形展开、多选、搜索过滤、分页加载表列表、全选/清空 |
|
||||||
|
| Step 4 | 确认信息 | 汇总展示已选数据库和表标签 |
|
||||||
|
|
||||||
|
#### 1.3 内置数据源创建/编辑/添加表 (`CreateBuiltinDataSource.vue`)
|
||||||
|
| 模式 | 步骤 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| create | 名称描述→业务场景→表结构 | AI 生成表结构,批量创建数据库和表 |
|
||||||
|
| edit | 名称描述→表结构 | 更新已有内置数据源 |
|
||||||
|
| addTable | 业务场景→表结构 | 在已有内置数据源下新增表 |
|
||||||
|
|
||||||
|
- **AI 能力**:AI 优化描述、AI 生成数据库名、AI 生成表名/表描述/字段描述
|
||||||
|
- **表结构编辑器**:左右分栏(左侧表列表, 右侧 TableDetailEditor 预览/编辑)
|
||||||
|
- **提交流程**:创建数据源 → 创建 PostgreSQL 连接 → 创建数据库 → 批量建表 → 检查 AI 训练状态
|
||||||
|
|
||||||
|
### 2. 数据库详情 (`DatabaseDetail.vue`)
|
||||||
|
|
||||||
|
左侧面板 + 右侧面板的经典布局:
|
||||||
|
|
||||||
|
#### 2.1 左侧 - 数据库与表管理
|
||||||
|
- **数据库选择器**:下拉切换当前数据库,悬浮显示数据库详情(ID/名称/数据源/类型)
|
||||||
|
- **数据表列表**:
|
||||||
|
- 搜索过滤
|
||||||
|
- 多选/全选 (Checkbox)
|
||||||
|
- 无限滚动加载
|
||||||
|
- AI 补全(选中多个表批量触发)
|
||||||
|
- 更多操作: AI智能建表、智能导入表、删除库
|
||||||
|
- **底部功能按钮**:字段关联管理、AI补全管理
|
||||||
|
|
||||||
|
#### 2.2 右侧 - 详情视图(三种视图)
|
||||||
|
|
||||||
|
**视图 A: 表详情 (`fieldViewMode = fields`)**
|
||||||
|
- 字段列表表格: 字段名、类型、长度、可空、主键、默认值、注释、描述
|
||||||
|
- AI 训练状态徽章(已训练/未训练)
|
||||||
|
- 操作: 修改表、字段关联管理、表订阅(已订阅/未订阅)、刷新
|
||||||
|
- 用户 ID 字段关联提示(user_id/createById 等自动关联系统用户)
|
||||||
|
|
||||||
|
**视图 B: 线上数据 (`fieldViewMode = online`)**
|
||||||
|
- 通过 `CustomizeDbTable` 组件查看/编辑生产环境真实数据
|
||||||
|
- 支持增删改查、导出 Excel、智能导入数据
|
||||||
|
|
||||||
|
**视图 C: 调试数据 (`fieldViewMode = debug`)**
|
||||||
|
- 同上,但操作测试环境数据
|
||||||
|
|
||||||
|
#### 2.3 右侧 - 字段关联管理视图
|
||||||
|
- 为当前表的字段建立与目标表字段的映射关系
|
||||||
|
- 目标表数据更新时自动同步到当前表
|
||||||
|
- 支持保存配置、添加/删除关联
|
||||||
|
|
||||||
|
#### 2.4 右侧 - AI 补全管理视图
|
||||||
|
- AI 补全任务列表: 任务名、类型、状态(待执行/运行中/成功/失败)、进度
|
||||||
|
- 任务详情弹窗: 训练结果(格式化代码展示)、耗时、场景描述
|
||||||
|
- 支持重新执行、删除任务
|
||||||
|
|
||||||
|
### 3. 内建表数据管理 (`CustomizeDbTable.vue`)
|
||||||
|
- 基于 `CommonDbTable` 的完整 CRUD 表格
|
||||||
|
- 动态列配置: 根据表结构自动生成字段类型、宽度、可编辑性
|
||||||
|
- **操作能力**: 新增行、编辑行、删除行、导出 Excel、导入数据、刷新
|
||||||
|
- **导入流程**: 下载模板 → 上传 Excel → AI 识别预览 → 确认导入(调用 postImportDocumentConfirm)
|
||||||
|
- 环境切换: `target` prop 控制 prod/test
|
||||||
|
|
||||||
|
### 4. 智能导入识别 (`TableRecognition.vue`)
|
||||||
|
- **Step 1: 上传文件**: 选择目标环境(prod/test),上传 Excel (.xlsx/.xls, <500KB)
|
||||||
|
- **Step 2: 智能识别**: AI 解析 Excel,预览表结构/数据,支持编辑
|
||||||
|
- **两种模式**:
|
||||||
|
- `table` 模式: 根据 Excel 内容自动生成数据表结构
|
||||||
|
- `data` 模式: 根据规范模板识别并导入数据
|
||||||
|
- 空行过滤、字段标记、模板下载
|
||||||
|
|
||||||
|
### 5. 智能问数 / 业务执行 (`ChatDebugging.vue`)
|
||||||
|
- **三栏布局**: 左侧面板(可折叠) + 中间聊天区 + 右侧信息
|
||||||
|
- **左侧面板**:
|
||||||
|
- 数字员工列表(可收起)
|
||||||
|
- 新建会话按钮
|
||||||
|
- 会话列表(点击切换、删除)
|
||||||
|
- 技能列表(展开/折叠)
|
||||||
|
- 技能名称 + 编辑按钮
|
||||||
|
- 工具列表(名称、别名、编辑、删除)
|
||||||
|
- 至少保留一个工具
|
||||||
|
- **中间聊天区** (`ChatBusiness` 组件):
|
||||||
|
- 数据源插件嵌入输入区 (`DataSourcePlugIn`)
|
||||||
|
- 数据库下拉选择
|
||||||
|
- 环境切换(生产/测试,仅内置数据源)
|
||||||
|
- SQL 消息控制器 (`SqlControllerMsg`)
|
||||||
|
- 默认数据库查询提示词(常用查询/数据统计/新手指引)
|
||||||
|
|
||||||
|
#### 5.1 SQL 查询结果展示 (`SqlControllerMsg.vue`)
|
||||||
|
| 视图类型 | 说明 |
|
||||||
|
|----------|------|
|
||||||
|
| SQL 代码 | 展示可执行 SQL 语句,支持复制 |
|
||||||
|
| 图表可视化 | ChartGallery 组件渲染图表 |
|
||||||
|
| 查询数据 | JSON 格式原始数据,支持复制 |
|
||||||
|
| 文本说明 | 业务名称、描述、AI 解释 |
|
||||||
|
| 表格视图 | Ant Design Table 展示查询结果 |
|
||||||
|
| 添加到工具 | 将 SQL 查询保存为可复用工具 |
|
||||||
|
|
||||||
|
- **添加工具功能**:
|
||||||
|
- 重复检测(按 sqlTemplate 归一化比对)
|
||||||
|
- 仅生产环境可用
|
||||||
|
- 技能创建/更新流程(无技能时自动创建 skill + MCP 配置)
|
||||||
|
- 通过 EventBus 触发技能刷新
|
||||||
|
|
||||||
|
#### 5.2 数据源选择插件 (`DataSourcePlugIn.vue`)
|
||||||
|
- 紧凑的内联选择器: 数据源名称 → 数据库下拉 → 环境(prod/test)
|
||||||
|
- 监听 props 变化自动重置无效选择
|
||||||
|
- 暴露 getSelection/clearSelection/setEnvironment 方法
|
||||||
|
|
||||||
|
### 6. API 密钥管理 (`DataSourceKeys.vue` + `DataSourceKeySetting.vue`)
|
||||||
|
- **密钥列表**: 名称、API Key(脱敏显示/可切换显示)、状态开关(启用/禁用)、创建时间、操作(详情/编辑/删除)
|
||||||
|
- **两步创建流程**:
|
||||||
|
1. 输入密钥名称 → 调用 postApiKey 创建
|
||||||
|
2. 弹窗询问是否配置权限 → 打开权限配置
|
||||||
|
- **权限配置** (`DataSourceKeySetting.vue`) - 3步向导:
|
||||||
|
- Step 1: 选择数据源(单选) + 配置数据源级权限
|
||||||
|
- Step 2: 选择数据库(单选, 非必填) + 配置数据库级权限
|
||||||
|
- Step 3: 选择数据表(多选, 非必填) + 行内配置表级权限
|
||||||
|
- Step 4: 完成页(ApiKeyPermissionPreview 汇总) → 批量提交权限
|
||||||
|
- **权限选项**: 按层级不同(connection/database/table),每种有对应的读/写/管理等权限
|
||||||
|
- **API 调用文档**: 切换到文档视图查看调用说明
|
||||||
|
|
||||||
|
### 7. 表字段编辑器 (`TableDetailEditor.vue`)
|
||||||
|
- **表信息编辑**: 表名(AI生成)、表描述(AI优化)
|
||||||
|
- **字段列表表格**:
|
||||||
|
- 序号、字段名、字段类型(分组下拉)、长度、主键、自增、可空、默认值、注释
|
||||||
|
- AI 生成字段描述(逐字段)
|
||||||
|
- 添加字段(首个字段自动设为主键+自增)
|
||||||
|
- 删除字段(有id的字段记录到 deletedColumns)
|
||||||
|
- 主键设置后不可取消(已有字段)
|
||||||
|
- 自增字段自动切换为整数类型
|
||||||
|
- **两种模式**: `edit` (编辑) / `preview` (预览)
|
||||||
|
- **校验功能**: validateFields 供外部调用
|
||||||
|
- **字段类型** (面向 PostgreSQL): VARCHAR/TEXT/SERIAL/INTEGER/BIGINT/NUMERIC/TIMESTAMP/BOOLEAN/JSONB/INT2/INT4/INT8/SMALLINT/SMALLSERIAL/BIGSERIAL/BOOL/BIGSERIAL
|
||||||
|
|
||||||
|
### 8. 其他辅助组件
|
||||||
|
|
||||||
|
| 组件 | 功能 |
|
||||||
|
|------|------|
|
||||||
|
| `ChartGallery.vue` | 图表可视化渲染 |
|
||||||
|
| `AddToolModal.vue` | 添加工具弹窗(工具名、描述编辑+AI辅助) |
|
||||||
|
| `ApiKeyPermissionPreview.vue` | 权限汇总预览组件 |
|
||||||
|
| `ApiCallDocument.vue` | API 调用文档展示 |
|
||||||
|
| `DatabaseMessageList.vue` | 数据库消息列表 |
|
||||||
|
| `field-types-dictionary.json` | 字段类型字典 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 三、API 接口层 (server/database.ts)
|
||||||
|
|
||||||
|
### 数据源管理
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `getConnectionList` | GET | 数据源列表(分页+搜索+筛选) |
|
||||||
|
| `getConnectionDetail` | GET | 数据源详情 |
|
||||||
|
| `postConnectionDetail` | POST | 创建数据源 |
|
||||||
|
| `putConnectionDetail` | PUT | 更新数据源 |
|
||||||
|
| `deleteConnection` | DELETE | 删除数据源 |
|
||||||
|
| `putConnectionChangeStatus` | PUT | 切换数据源状态(启用/停用) |
|
||||||
|
| `testConnection` | POST | 测试连接 |
|
||||||
|
|
||||||
|
### 内置数据库管理
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `postCreateBuiltinPostgreSQLConnection` | POST | 创建内置 PostgreSQL 连接 |
|
||||||
|
| `postCreateDatabase` | POST | 创建数据库 |
|
||||||
|
| `putAlterDatabase` | PUT | 修改数据库 |
|
||||||
|
| `postCreateTable` | POST | 创建表 |
|
||||||
|
| `putAlterTable` | PUT | 修改表 |
|
||||||
|
| `postGenerateTable` | POST | AI 生成表结构 |
|
||||||
|
| `putUpdateBuiltinDatabase` | PUT | 更新内置数据库 |
|
||||||
|
|
||||||
|
### 数据库配置
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `getConnectionConfig` | GET | 获取数据库配置(含 skillBool) |
|
||||||
|
| `getConnectionConfigList` | GET | 数据库配置列表 |
|
||||||
|
| `postConnectionConfig` | POST | 创建数据库配置 |
|
||||||
|
| `putConnectionConfig` | PUT | 更新数据库配置 |
|
||||||
|
| `deleteConnectionConfig` | DELETE | 删除数据库配置 |
|
||||||
|
| `getConnectionRealtimeStructure` | GET | 获取实时数据库结构 |
|
||||||
|
|
||||||
|
### 表数据管理
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `getTableList` | GET | 表列表 |
|
||||||
|
| `getTableDetail` | GET | 表详情 |
|
||||||
|
| `getBuiltinTableData` | GET | 获取内置表数据(分页) |
|
||||||
|
| `postBuiltinTableRows` | POST | 新增表数据行 |
|
||||||
|
| `putBuiltinTableRows` | PUT | 更新表数据行 |
|
||||||
|
| `deleteBuiltinTableRows` | DELETE | 删除表数据行 |
|
||||||
|
| `getBuiltinTableExportExcel` | GET | 导出表数据为 Excel |
|
||||||
|
|
||||||
|
### 数据导入
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `postImportDocumentPreview` | POST | 上传 Excel 预览识别结果 |
|
||||||
|
| `postImportDocumentConfirm` | POST | 确认导入数据 |
|
||||||
|
|
||||||
|
### 技能与工具
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `getSkillByDatasource` | GET | 根据数据源获取技能 |
|
||||||
|
| `getSkillBySkillId` | GET | 根据技能ID获取工具列表 |
|
||||||
|
| `postSkillCreateOrGet` | POST | 创建或获取技能 |
|
||||||
|
| `putSkillUpdateOrGet` | PUT | 更新技能配置 |
|
||||||
|
| `postSkillToolUpdateOrGet` | POST | 创建/更新技能工具 |
|
||||||
|
| `postDeleteSkillTool` | POST | 删除技能工具 |
|
||||||
|
| `postSqlSkillConfirmTools` | POST | 确认并创建 SQL 工具 |
|
||||||
|
|
||||||
|
### AI 训练
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `getAiTrainingList` | GET | AI 训练任务列表 |
|
||||||
|
| `postAiTrainingCreateBySelected` | POST | 创建 AI 训练任务 |
|
||||||
|
| `getAiTrainingDetail` | GET | 训练任务详情 |
|
||||||
|
|
||||||
|
### API 密钥
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `getApiKeyList` | GET | 密钥列表 |
|
||||||
|
| `postApiKey` | POST | 创建密钥 |
|
||||||
|
| `putApiKey` | PUT | 更新密钥 |
|
||||||
|
| `deleteApiKey` | DELETE | 删除密钥 |
|
||||||
|
| `getApiKeyPermission` | GET | 获取密钥权限 |
|
||||||
|
| `postApiKeyPermissionGrantBatch` | POST | 批量授予权限 |
|
||||||
|
|
||||||
|
### 订阅
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `postDatasourceSubscriptionToggle` | POST | 切换表订阅状态 |
|
||||||
|
|
||||||
|
### SQL 执行
|
||||||
|
| 函数 | 方法 | 用途 |
|
||||||
|
|------|------|------|
|
||||||
|
| `executeSql` | POST | 执行 SQL 查询 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 四、状态管理与通信
|
||||||
|
|
||||||
|
- **Pinia Store**: `useDatabaseChatStore` 管理选中数据库、环境、技能状态
|
||||||
|
- **EventBus**: `EVENTS.RELOAD_SKILL_DATA` 用于工具添加后刷新技能数据
|
||||||
|
- **权限控制**: `hasPermission()` 函数按权限标识控制按钮可见性
|
||||||
|
|
||||||
|
## 五、权限标识体系
|
||||||
|
|
||||||
|
| 权限标识 | 控制范围 |
|
||||||
|
|----------|----------|
|
||||||
|
| `database:create` | 创建数据源 |
|
||||||
|
| `database:edit` | 编辑数据源 |
|
||||||
|
| `database:delete` | 删除数据源 |
|
||||||
|
| `database:import` | 导入表/数据 |
|
||||||
|
| `database:export` | 导出表 |
|
||||||
|
| `database:table:create` | AI 智能建表 |
|
||||||
|
| `database:table:ai:complete` | AI 补全功能 |
|
||||||
|
| `database:apikey:view` | 查看密钥管理 |
|
||||||
|
| `database:apikey:create` | 创建密钥 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 六、关键技术特点
|
||||||
|
|
||||||
|
1. **AI 深度集成**: 描述润色、名称生成、表结构自动生成、字段描述生成、AI 补全、AI 训练
|
||||||
|
2. **双环境隔离**: 生产/测试环境切换,测试环境限制添加工具等敏感操作
|
||||||
|
3. **无限滚动**: IntersectionObserver + 哨兵元素实现列表分页加载
|
||||||
|
4. **三级权限体系**: 数据源 → 数据库 → 数据表,通过 API Key 控制访问粒度
|
||||||
|
5. **MCP 工具集成**: 内置数据源自动配置 MCP Server (`lzwcai-mcp-sqlexecutor`),支持 AI 工具注册
|
||||||
|
6. **Excel 智能导入**: AI 识别 Excel 内容,自动匹配表结构,支持表结构和数据两种导入模式
|
||||||
14
lzwcai_mcp_agile_db/main.py
Normal file
14
lzwcai_mcp_agile_db/main.py
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
"""
|
||||||
|
Entry point for lzwcai-mcp-agile-db
|
||||||
|
Runs the MCP server for database management platform
|
||||||
|
"""
|
||||||
|
|
||||||
|
from lzwcai_mcp_agile_db.server import main
|
||||||
|
import os
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
# 账号密码方式:客户端会在首次请求时自动调用 /login 换取 token
|
||||||
|
os.environ["AGILE_DB_ACCOUNT"] = "yy8z9"
|
||||||
|
os.environ["AGILE_DB_PASSWORD"] = "lzwc@2025."
|
||||||
|
os.environ["backendBaseUrl"] = "http://192.168.2.236:8082/api"
|
||||||
|
main()
|
||||||
34
lzwcai_mcp_agile_db/pyproject.toml
Normal file
34
lzwcai_mcp_agile_db/pyproject.toml
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "lzwcai-mcp-agile-db"
|
||||||
|
version = "0.1.17"
|
||||||
|
description = "MCP server for database management platform with 33 tools for datasource, table, data, API key, and skill management"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
authors = [
|
||||||
|
{name = "lzwcai", email = "your-email@example.com"},
|
||||||
|
]
|
||||||
|
keywords = ["mcp", "database", "agile", "server"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
"Programming Language :: Python :: 3.13",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"httpx>=0.28.1",
|
||||||
|
"mcp[cli]>=1.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
lzwcai-mcp-agile-db = "lzwcai_mcp_agile_db.server:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["lzwcai_mcp_agile_db"]
|
||||||
1
lzwcai_mcp_agile_db_third/.python-version
Normal file
1
lzwcai_mcp_agile_db_third/.python-version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
75
lzwcai_mcp_agile_db_third/README.md
Normal file
75
lzwcai_mcp_agile_db_third/README.md
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
# lzwcai-mcp-agile-db-third
|
||||||
|
|
||||||
|
AgileDB 数据源管理 MCP Server,基于 `API_DOCUMENTATION.md` 将后端数据源/连接/DDL/DML/API 接口封装为 34 个 MCP 工具。
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
本服务将后端 `datasource` 模块的 API 接口代理为标准 MCP 工具,分为两大类:
|
||||||
|
|
||||||
|
### 1. 数据源配置管理
|
||||||
|
|
||||||
|
- `list_datasource_configs`:查询数据源配置列表
|
||||||
|
- `get_datasource_config`:获取数据源配置详情
|
||||||
|
- `batch_create_datasource_configs`:批量创建数据源配置
|
||||||
|
- `replace_datasource_configs`:全量替换数据源配置
|
||||||
|
- `batch_update_datasource_configs`:批量修改数据源配置
|
||||||
|
- `delete_datasource_configs`:批量删除数据源配置
|
||||||
|
- `test_connection_config`:测试数据库连接
|
||||||
|
- `change_datasource_status`:修改数据源状态
|
||||||
|
- `export_datasource_configs`:导出数据源配置为 Excel
|
||||||
|
|
||||||
|
### 2. 数据库连接实例管理
|
||||||
|
|
||||||
|
- `list_connections` / `get_connection` / `create_connection` / `update_connection` / `delete_connection`:连接实例 CRUD
|
||||||
|
- `test_connection` / `change_connection_status`:连接测试与状态切换
|
||||||
|
- `realtime_structure` / `realtime_databases` / `realtime_tables`:实时查询库表结构
|
||||||
|
- `create_builtin_postgresql` / `update_builtin_database`:内置 PostgreSQL 连接管理
|
||||||
|
- `execute_sql`:执行原生 SQL
|
||||||
|
- `create_database` / `create_table` / `create_database_table` / `alter_database` / `alter_table`:DDL 操作
|
||||||
|
- `generate_table`:AI 生成表结构
|
||||||
|
- `import_document_preview` / `import_document_confirm`:Excel/CSV 文档导入
|
||||||
|
- `builtin_table_data` / `builtin_table_insert` / `builtin_table_update` / `builtin_table_delete`:表数据 CRUD
|
||||||
|
|
||||||
|
## 环境配置
|
||||||
|
|
||||||
|
| 环境变量 | 说明 | 默认值 |
|
||||||
|
|----------|------|--------|
|
||||||
|
| `backendBaseUrl` | 后端 API 基础地址 | `http://lzwcai-demp-corp-manager:8086` |
|
||||||
|
| `datasourceApiKey` | 默认 `X-Datasource-API-Key`,可选 | 空 |
|
||||||
|
| `LOG_LEVEL` | 日志级别 | `INFO` |
|
||||||
|
|
||||||
|
## 安装与运行
|
||||||
|
|
||||||
|
使用 uv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
uv run python -m lzwcai_mcp_agile_db_third.main
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用 pip:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\python -m pip install mcp httpx
|
||||||
|
.venv\Scripts\python -m lzwcai_mcp_agile_db_third.main
|
||||||
|
```
|
||||||
|
|
||||||
|
安装为命令后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lzwcai-mcp-agile-db-third
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用 mcp CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp dev lzwcai_mcp_agile_db_third/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 所有写操作(创建/修改/删除)会先落到 `prod` 环境;带 `target` 参数的工具可切换为 `test`
|
||||||
|
- `import_document_preview` 通过本地文件路径上传 Excel/CSV
|
||||||
|
- 执行删除类工具前,调用方应遵循安全确认原则,向用户展示影响范围并二次确认
|
||||||
|
- 日志文件中会对 `password`、`apiKey`、`token`、`secret` 等敏感字段进行脱敏
|
||||||
BIN
lzwcai_mcp_agile_db_third/__pycache__/main.cpython-312.pyc
Normal file
BIN
lzwcai_mcp_agile_db_third/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
21
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/.gitignore
vendored
Normal file
21
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*.egg-info/
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv/
|
||||||
|
venv/
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
logs/
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# OS
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
|||||||
|
# lzwcai-mcp-agile-db-third
|
||||||
|
|
||||||
|
AgileDB 数据源管理 MCP Server,基于 `API_DOCUMENTATION.md` 将后端数据源/连接/DDL/DML/API 接口封装为 34 个 MCP 工具。
|
||||||
|
|
||||||
|
## 功能概述
|
||||||
|
|
||||||
|
本服务将后端 `datasource` 模块的 API 接口代理为标准 MCP 工具,分为两大类:
|
||||||
|
|
||||||
|
### 1. 数据源配置管理
|
||||||
|
|
||||||
|
- `list_datasource_configs`:查询数据源配置列表
|
||||||
|
- `get_datasource_config`:获取数据源配置详情
|
||||||
|
- `batch_create_datasource_configs`:批量创建数据源配置
|
||||||
|
- `replace_datasource_configs`:全量替换数据源配置
|
||||||
|
- `batch_update_datasource_configs`:批量修改数据源配置
|
||||||
|
- `delete_datasource_configs`:批量删除数据源配置
|
||||||
|
- `test_connection_config`:测试数据库连接
|
||||||
|
- `change_datasource_status`:修改数据源状态
|
||||||
|
- `export_datasource_configs`:导出数据源配置为 Excel
|
||||||
|
|
||||||
|
### 2. 数据库连接实例管理
|
||||||
|
|
||||||
|
- `list_connections` / `get_connection` / `create_connection` / `update_connection` / `delete_connection`:连接实例 CRUD
|
||||||
|
- `test_connection` / `change_connection_status`:连接测试与状态切换
|
||||||
|
- `realtime_structure` / `realtime_databases` / `realtime_tables`:实时查询库表结构
|
||||||
|
- `create_builtin_postgresql` / `update_builtin_database`:内置 PostgreSQL 连接管理
|
||||||
|
- `execute_sql`:执行原生 SQL
|
||||||
|
- `create_database` / `create_table` / `create_database_table` / `alter_database` / `alter_table`:DDL 操作
|
||||||
|
- `generate_table`:AI 生成表结构
|
||||||
|
- `import_document_preview` / `import_document_confirm`:Excel/CSV 文档导入
|
||||||
|
- `builtin_table_data` / `builtin_table_insert` / `builtin_table_update` / `builtin_table_delete`:表数据 CRUD
|
||||||
|
|
||||||
|
## 环境配置
|
||||||
|
|
||||||
|
| 环境变量 | 说明 | 默认值 |
|
||||||
|
|----------|------|--------|
|
||||||
|
| `backendBaseUrl` | 后端 API 基础地址 | `http://lzwcai-demp-corp-manager:8086` |
|
||||||
|
| `datasourceApiKey` | 默认 `X-Datasource-API-Key`,可选 | 空 |
|
||||||
|
| `LOG_LEVEL` | 日志级别 | `INFO` |
|
||||||
|
|
||||||
|
## 安装与运行
|
||||||
|
|
||||||
|
使用 uv:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uv sync
|
||||||
|
uv run python -m lzwcai_mcp_agile_db_third.main
|
||||||
|
```
|
||||||
|
|
||||||
|
或使用 pip:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m venv .venv
|
||||||
|
.venv\Scripts\python -m pip install mcp httpx
|
||||||
|
.venv\Scripts\python -m lzwcai_mcp_agile_db_third.main
|
||||||
|
```
|
||||||
|
|
||||||
|
安装为命令后:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
lzwcai-mcp-agile-db-third
|
||||||
|
```
|
||||||
|
|
||||||
|
## 使用 mcp CLI
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mcp dev lzwcai_mcp_agile_db_third/main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
## 注意事项
|
||||||
|
|
||||||
|
- 所有写操作(创建/修改/删除)会先落到 `prod` 环境;带 `target` 参数的工具可切换为 `test`
|
||||||
|
- `import_document_preview` 通过本地文件路径上传 Excel/CSV
|
||||||
|
- 执行删除类工具前,调用方应遵循安全确认原则,向用户展示影响范围并二次确认
|
||||||
|
- 日志文件中会对 `password`、`apiKey`、`token`、`secret` 等敏感字段进行脱敏
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""lzwcai_mcp_agile_db_third package."""
|
||||||
216
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/main.py
Normal file
216
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/main.py
Normal file
@@ -0,0 +1,216 @@
|
|||||||
|
"""MCP server entrypoint for Agile DB third-party datasource APIs."""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .tools import list_tools, get_tool
|
||||||
|
from .utils import DataSourceAPIClient, get_env_config, get_api_key
|
||||||
|
from .utils.logger_config import logger_config
|
||||||
|
except ImportError:
|
||||||
|
from tools import list_tools, get_tool
|
||||||
|
from utils import DataSourceAPIClient, get_env_config, get_api_key
|
||||||
|
from utils.logger_config import logger_config
|
||||||
|
|
||||||
|
from mcp.server import NotificationOptions, Server
|
||||||
|
from mcp.server.models import InitializationOptions
|
||||||
|
import mcp.types as types
|
||||||
|
|
||||||
|
mcp_logger = logger_config.setup_mcp_logging()
|
||||||
|
|
||||||
|
|
||||||
|
def _text_response(payload: Dict[str, Any]) -> List[types.TextContent]:
|
||||||
|
"""Build a JSON text response."""
|
||||||
|
return [
|
||||||
|
types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(payload, ensure_ascii=False, indent=2),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _build_tool(tool_def: Dict[str, Any]) -> types.Tool:
|
||||||
|
"""Build an MCP Tool from a tool definition."""
|
||||||
|
return types.Tool(
|
||||||
|
name=tool_def["name"],
|
||||||
|
description=tool_def["description"],
|
||||||
|
inputSchema=tool_def["inputSchema"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
server = Server("lzwcai-mcp-agile-db-third")
|
||||||
|
_api_client: Optional[DataSourceAPIClient] = None
|
||||||
|
|
||||||
|
|
||||||
|
def _get_api_client() -> DataSourceAPIClient:
|
||||||
|
"""Get or create the default API client."""
|
||||||
|
global _api_client
|
||||||
|
if _api_client is None:
|
||||||
|
env = get_env_config()
|
||||||
|
_api_client = DataSourceAPIClient(base_url=env.get("backend_base_url"))
|
||||||
|
return _api_client
|
||||||
|
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def handle_list_tools() -> List[types.Tool]:
|
||||||
|
"""List all available MCP tools."""
|
||||||
|
try:
|
||||||
|
mcp_logger.info("收到列出工具请求")
|
||||||
|
tools = [_build_tool(tool_def) for tool_def in list_tools()]
|
||||||
|
mcp_logger.info(f"成功生成 {len(tools)} 个 MCP 工具")
|
||||||
|
return tools
|
||||||
|
except Exception as e:
|
||||||
|
mcp_logger.error(f"列出工具失败: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
@server.call_tool()
|
||||||
|
async def handle_call_tool(
|
||||||
|
name: str,
|
||||||
|
arguments: Optional[Dict[str, Any]],
|
||||||
|
) -> List[types.TextContent]:
|
||||||
|
"""Handle MCP tool invocation by proxying to the backend API."""
|
||||||
|
try:
|
||||||
|
mcp_logger.info(f"收到工具调用请求: {name}")
|
||||||
|
mcp_logger.debug(f"工具参数: {arguments}")
|
||||||
|
|
||||||
|
tool_def = get_tool(name)
|
||||||
|
if tool_def is None:
|
||||||
|
error_msg = f"未找到工具: {name}"
|
||||||
|
mcp_logger.warning(error_msg)
|
||||||
|
return _text_response({"error": error_msg})
|
||||||
|
|
||||||
|
# Validate required parameters
|
||||||
|
args = arguments or {}
|
||||||
|
schema = tool_def["inputSchema"]
|
||||||
|
required = schema.get("required", [])
|
||||||
|
missing = [key for key in required if key not in args or args[key] is None]
|
||||||
|
if missing:
|
||||||
|
error_msg = f"工具 {name} 缺少必填参数: {', '.join(missing)}"
|
||||||
|
mcp_logger.warning(error_msg)
|
||||||
|
return _text_response({"error": error_msg, "missing": missing})
|
||||||
|
|
||||||
|
# Categorize parameters
|
||||||
|
path_params: Dict[str, Any] = {}
|
||||||
|
query_params: Dict[str, Any] = {}
|
||||||
|
body_params: Dict[str, Any] = {}
|
||||||
|
file_path: Optional[str] = None
|
||||||
|
api_key: Optional[str] = None
|
||||||
|
|
||||||
|
categories = tool_def["paramCategories"]
|
||||||
|
for key, value in args.items():
|
||||||
|
if value is None:
|
||||||
|
continue
|
||||||
|
category = categories.get(key)
|
||||||
|
if category == "path":
|
||||||
|
path_params[key] = value
|
||||||
|
elif category == "query":
|
||||||
|
query_params[key] = value
|
||||||
|
elif category == "body":
|
||||||
|
body_params[key] = value
|
||||||
|
elif category == "file":
|
||||||
|
file_path = value
|
||||||
|
elif category == "header":
|
||||||
|
api_key = value
|
||||||
|
|
||||||
|
# Apply schema-defined defaults for parameters the caller omitted,
|
||||||
|
# so "有默认值的参数不填就用默认值"(如 datasourceType、target、分页)。
|
||||||
|
properties = schema.get("properties", {})
|
||||||
|
for key, prop in properties.items():
|
||||||
|
if "default" not in prop:
|
||||||
|
continue
|
||||||
|
if args.get(key) is not None:
|
||||||
|
continue
|
||||||
|
default_value = prop["default"]
|
||||||
|
category = categories.get(key)
|
||||||
|
if category == "path":
|
||||||
|
path_params[key] = default_value
|
||||||
|
elif category == "query":
|
||||||
|
query_params[key] = default_value
|
||||||
|
elif category == "body":
|
||||||
|
body_params[key] = default_value
|
||||||
|
elif category == "header" and not api_key:
|
||||||
|
api_key = default_value
|
||||||
|
|
||||||
|
# Fall back to the environment-configured API key when the tool call
|
||||||
|
# doesn't carry an explicit header value.
|
||||||
|
if not api_key:
|
||||||
|
api_key = get_api_key() or None
|
||||||
|
if api_key:
|
||||||
|
mcp_logger.info("工具调用未带 API Key,已回退到环境变量 datasourceApiKey")
|
||||||
|
|
||||||
|
client = _get_api_client()
|
||||||
|
result = client.request(
|
||||||
|
method=tool_def["method"],
|
||||||
|
path_template=tool_def["path"],
|
||||||
|
path_params=path_params,
|
||||||
|
query_params=query_params,
|
||||||
|
body=body_params if body_params else None,
|
||||||
|
file_path=file_path,
|
||||||
|
api_key=api_key,
|
||||||
|
)
|
||||||
|
|
||||||
|
return _text_response(result)
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"工具调用失败: {name}, 错误: {e}"
|
||||||
|
mcp_logger.error(error_msg, exc_info=True)
|
||||||
|
return _text_response({"error": error_msg})
|
||||||
|
|
||||||
|
|
||||||
|
async def async_main():
|
||||||
|
"""Async entry for the MCP server."""
|
||||||
|
try:
|
||||||
|
mcp_logger.info("=" * 60)
|
||||||
|
mcp_logger.info("正在启动 MCP 服务: lzwcai-mcp-agile-db-third")
|
||||||
|
mcp_logger.info("版本: 0.1.0")
|
||||||
|
mcp_logger.info("=" * 60)
|
||||||
|
|
||||||
|
env = get_env_config()
|
||||||
|
mcp_logger.info(f"环境配置 - Backend Base URL: {env.get('backend_base_url')}")
|
||||||
|
mcp_logger.info(f"环境配置 - API Key: {'已设置' if env.get('api_key') else '未设置'}")
|
||||||
|
mcp_logger.info("=" * 60)
|
||||||
|
|
||||||
|
from mcp.server.stdio import stdio_server
|
||||||
|
|
||||||
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
|
mcp_logger.info("MCP 服务已启动,等待客户端连接...")
|
||||||
|
await server.run(
|
||||||
|
read_stream,
|
||||||
|
write_stream,
|
||||||
|
InitializationOptions(
|
||||||
|
server_name="lzwcai-mcp-agile-db-third",
|
||||||
|
server_version="0.1.2",
|
||||||
|
capabilities=server.get_capabilities(
|
||||||
|
notification_options=NotificationOptions(),
|
||||||
|
experimental_capabilities={},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
mcp_logger.info("MCP 服务已关闭")
|
||||||
|
except Exception as e:
|
||||||
|
mcp_logger.error(f"MCP 服务运行失败: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""Console entrypoint."""
|
||||||
|
try:
|
||||||
|
logger_config.setup_logging(
|
||||||
|
app_name="lzwcai_mcp_agile_db_third",
|
||||||
|
log_level=logging.INFO,
|
||||||
|
console_output=False,
|
||||||
|
)
|
||||||
|
mcp_logger.info("开始运行 MCP Agile DB Third 服务")
|
||||||
|
asyncio.run(async_main())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
mcp_logger.info("收到中断信号,正在关闭服务...")
|
||||||
|
except Exception as e:
|
||||||
|
mcp_logger.error(f"程序运行失败: {e}", exc_info=True)
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
137
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/mock_db.py
Normal file
137
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/mock_db.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
"""Docker-based PostgreSQL mock for local self-testing.
|
||||||
|
|
||||||
|
Provides a throwaway Postgres container that the backend can connect to during
|
||||||
|
external-connection tests. The container is bound to 0.0.0.0:5432 on the Docker
|
||||||
|
host, so the host IP must be reachable from the backend server.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
DEFAULT_IMAGE = "postgres:16-alpine"
|
||||||
|
DEFAULT_CONTAINER = "lzwcai_mcp_agile_db_third_mock_pg"
|
||||||
|
DEFAULT_PORT = 5432
|
||||||
|
DEFAULT_DATABASE = "postgres"
|
||||||
|
DEFAULT_USERNAME = "postgres"
|
||||||
|
DEFAULT_PASSWORD = "postgres"
|
||||||
|
|
||||||
|
|
||||||
|
def _run(args: list, check: bool = False, capture: bool = True) -> subprocess.CompletedProcess:
|
||||||
|
"""Run a shell command and return the result."""
|
||||||
|
return subprocess.run(
|
||||||
|
args,
|
||||||
|
check=check,
|
||||||
|
capture_output=capture,
|
||||||
|
text=True,
|
||||||
|
shell=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def host_ip(prefer_prefixes: Optional[list] = None) -> str:
|
||||||
|
"""Return a non-loopback IPv4 address of this machine.
|
||||||
|
|
||||||
|
Prefers addresses starting with the given prefixes so users can steer the
|
||||||
|
result toward the network segment that the backend can reach.
|
||||||
|
"""
|
||||||
|
prefer_prefixes = prefer_prefixes or ["192.168.", "10.", "172."]
|
||||||
|
hostname = socket.gethostname()
|
||||||
|
_, _, ips = socket.gethostbyname_ex(hostname)
|
||||||
|
# Prefer addresses matching a requested prefix, then any non-loopback.
|
||||||
|
for prefix in prefer_prefixes:
|
||||||
|
for ip in ips:
|
||||||
|
if ip.startswith(prefix) and not ip.startswith("127."):
|
||||||
|
return ip
|
||||||
|
for ip in ips:
|
||||||
|
if not ip.startswith("127."):
|
||||||
|
return ip
|
||||||
|
raise RuntimeError("Could not find a non-loopback IPv4 address on this host")
|
||||||
|
|
||||||
|
|
||||||
|
def is_running(container: str = DEFAULT_CONTAINER) -> bool:
|
||||||
|
"""Check whether the mock container is currently running."""
|
||||||
|
result = _run(["docker", "ps", "--filter", f"name={container}", "--format", "{{.Names}}"])
|
||||||
|
return container in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def start(
|
||||||
|
container: str = DEFAULT_CONTAINER,
|
||||||
|
port: int = DEFAULT_PORT,
|
||||||
|
username: str = DEFAULT_USERNAME,
|
||||||
|
password: str = DEFAULT_PASSWORD,
|
||||||
|
database: str = DEFAULT_DATABASE,
|
||||||
|
image: str = DEFAULT_IMAGE,
|
||||||
|
) -> str:
|
||||||
|
"""Start the Postgres mock container if not already running.
|
||||||
|
|
||||||
|
Returns the host IP that should be supplied to backend connection tests.
|
||||||
|
"""
|
||||||
|
if is_running(container):
|
||||||
|
print(f"Mock DB container '{container}' is already running.")
|
||||||
|
return host_ip()
|
||||||
|
|
||||||
|
# Remove any stale container with the same name.
|
||||||
|
_run(["docker", "rm", "-f", container], check=False)
|
||||||
|
|
||||||
|
print(f"Starting mock Postgres container '{container}' (image={image})...")
|
||||||
|
_run(
|
||||||
|
[
|
||||||
|
"docker", "run", "-d",
|
||||||
|
"--name", container,
|
||||||
|
"-p", f"{port}:{port}",
|
||||||
|
"-e", f"POSTGRES_USER={username}",
|
||||||
|
"-e", f"POSTGRES_PASSWORD={password}",
|
||||||
|
"-e", f"POSTGRES_DB={database}",
|
||||||
|
image,
|
||||||
|
],
|
||||||
|
check=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
print("Waiting for Postgres to become ready...")
|
||||||
|
deadline = time.time() + 30
|
||||||
|
while time.time() < deadline:
|
||||||
|
result = _run(
|
||||||
|
["docker", "exec", container, "pg_isready", "-U", username],
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
if result.returncode == 0:
|
||||||
|
break
|
||||||
|
time.sleep(0.5)
|
||||||
|
else:
|
||||||
|
stop(container)
|
||||||
|
raise RuntimeError("Postgres mock failed to become ready within 30s")
|
||||||
|
|
||||||
|
ip = host_ip()
|
||||||
|
print(f"Mock Postgres is ready at {ip}:{port} (user={username}, password={password}, db={database})")
|
||||||
|
return ip
|
||||||
|
|
||||||
|
|
||||||
|
def stop(container: str = DEFAULT_CONTAINER) -> None:
|
||||||
|
"""Stop and remove the mock Postgres container."""
|
||||||
|
print(f"Stopping mock DB container '{container}'...")
|
||||||
|
_run(["docker", "stop", "-t", "5", container], check=False)
|
||||||
|
_run(["docker", "rm", "-f", container], check=False)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
if len(sys.argv) < 2:
|
||||||
|
print("Usage: python -m lzwcai_mcp_agile_db_third.mock_db [start|stop|ip]")
|
||||||
|
sys.exit(1)
|
||||||
|
cmd = sys.argv[1].lower()
|
||||||
|
if cmd == "start":
|
||||||
|
start()
|
||||||
|
elif cmd == "stop":
|
||||||
|
stop()
|
||||||
|
elif cmd in ("ip", "host"):
|
||||||
|
print(host_ip())
|
||||||
|
elif cmd == "status":
|
||||||
|
print("running" if is_running() else "stopped")
|
||||||
|
else:
|
||||||
|
print(f"Unknown command: {cmd}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "lzwcai-mcp-agile-db-third"
|
||||||
|
version = "0.1.3"
|
||||||
|
description = "MCP server for Agile DB third-party datasource APIs"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
authors = [
|
||||||
|
{name = "lzwcai", email = "your-email@example.com"},
|
||||||
|
]
|
||||||
|
keywords = ["mcp", "datasource", "database", "agile-db"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"httpx>=0.28.1",
|
||||||
|
"mcp[cli]>=1.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
lzwcai-mcp-agile-db-third = "lzwcai_mcp_agile_db_third.main:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["lzwcai_mcp_agile_db_third"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel.force-include]
|
||||||
|
|
||||||
|
[tool.hatch.build]
|
||||||
|
exclude = [
|
||||||
|
"lzwcai_mcp_agile_db_third/logs/**",
|
||||||
|
"**/.__pycache__/**",
|
||||||
|
"lzwcai_mcp_agile_db_third/.gitignore",
|
||||||
|
".venv/**",
|
||||||
|
]
|
||||||
870
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/tools.py
Normal file
870
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/tools.py
Normal file
@@ -0,0 +1,870 @@
|
|||||||
|
"""MCP tool definitions for datasource API."""
|
||||||
|
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
|
||||||
|
def _tool(
|
||||||
|
name: str,
|
||||||
|
description: str,
|
||||||
|
method: str,
|
||||||
|
path: str,
|
||||||
|
properties: Dict[str, Any],
|
||||||
|
required: List[str],
|
||||||
|
param_categories: Dict[str, str],
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Build a tool definition dictionary."""
|
||||||
|
return {
|
||||||
|
"name": name,
|
||||||
|
"description": description,
|
||||||
|
"method": method,
|
||||||
|
"path": path,
|
||||||
|
"inputSchema": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": properties,
|
||||||
|
"required": required,
|
||||||
|
},
|
||||||
|
"paramCategories": param_categories,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# 公共参数 schema 片段
|
||||||
|
_API_KEY_PROP = {
|
||||||
|
"apiKey": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "可选的 X-Datasource-API-Key 权限密钥",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_TARGET_PROP = {
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "目标环境(prod/test,默认 prod)",
|
||||||
|
"default": "prod",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_PAGE_PROPS = {
|
||||||
|
"pageNum": {"type": "integer", "description": "页码(默认1)", "default": 1},
|
||||||
|
"pageSize": {"type": "integer", "description": "每页数量(默认10)", "default": 10},
|
||||||
|
}
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 1. 数据源配置管理
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
LIST_DATASOURCE_CONFIGS = _tool(
|
||||||
|
name="list_datasource_configs",
|
||||||
|
description="查询数据源配置列表,支持分页和多条件筛选",
|
||||||
|
method="GET",
|
||||||
|
path="/datasource/config/list",
|
||||||
|
properties={
|
||||||
|
"datasourceName": {"type": "string", "description": "数据源名称(模糊查询)"},
|
||||||
|
"datasourceId": {"type": "integer", "description": "连接ID"},
|
||||||
|
"datasourceType": {"type": "string", "description": "数据库类型"},
|
||||||
|
"status": {"type": "integer", "description": "状态"},
|
||||||
|
"showTable": {"type": "boolean", "description": "是否显示表数量"},
|
||||||
|
**_PAGE_PROPS,
|
||||||
|
},
|
||||||
|
required=[],
|
||||||
|
param_categories={
|
||||||
|
"datasourceName": "query",
|
||||||
|
"datasourceId": "query",
|
||||||
|
"datasourceType": "query",
|
||||||
|
"status": "query",
|
||||||
|
"showTable": "query",
|
||||||
|
"pageNum": "query",
|
||||||
|
"pageSize": "query",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
GET_DATASOURCE_CONFIG = _tool(
|
||||||
|
name="get_datasource_config",
|
||||||
|
description="获取指定数据源配置的详细信息",
|
||||||
|
method="GET",
|
||||||
|
path="/datasource/config/{id}",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "数据源配置ID"},
|
||||||
|
},
|
||||||
|
required=["id"],
|
||||||
|
param_categories={"id": "path"},
|
||||||
|
)
|
||||||
|
|
||||||
|
BATCH_CREATE_DATASOURCE_CONFIGS = _tool(
|
||||||
|
name="batch_create_datasource_configs",
|
||||||
|
description="批量创建数据源配置并同步表结构",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/config",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"datasourceNamePrefix": {"type": "string", "description": "数据源名称前缀"},
|
||||||
|
"enterpriseId": {"type": "integer", "description": "企业ID"},
|
||||||
|
"status": {"type": "integer", "description": "状态"},
|
||||||
|
"remark": {"type": "string", "description": "备注信息"},
|
||||||
|
"syncTables": {"type": "boolean", "description": "是否同步表结构"},
|
||||||
|
"databases": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "数据库与表列表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"databaseName": {"type": "string"},
|
||||||
|
"tableNames": {"type": "array", "items": {"type": "string"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required=["connectionId", "databases"],
|
||||||
|
param_categories={
|
||||||
|
"connectionId": "body",
|
||||||
|
"datasourceNamePrefix": "body",
|
||||||
|
"enterpriseId": "body",
|
||||||
|
"status": "body",
|
||||||
|
"remark": "body",
|
||||||
|
"syncTables": "body",
|
||||||
|
"databases": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
REPLACE_DATASOURCE_CONFIGS = _tool(
|
||||||
|
name="replace_datasource_configs",
|
||||||
|
description="全量替换指定连接下的数据源配置(未传入的配置将被删除),请谨慎操作",
|
||||||
|
method="PUT",
|
||||||
|
path="/datasource/config",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"datasourceNamePrefix": {"type": "string", "description": "数据源名称前缀"},
|
||||||
|
"enterpriseId": {"type": "integer", "description": "企业ID"},
|
||||||
|
"status": {"type": "integer", "description": "状态"},
|
||||||
|
"remark": {"type": "string", "description": "备注信息"},
|
||||||
|
"syncTables": {"type": "boolean", "description": "是否同步表结构"},
|
||||||
|
"databases": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "数据库与表列表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"databaseName": {"type": "string"},
|
||||||
|
"tableNames": {"type": "array", "items": {"type": "string"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required=["connectionId", "databases"],
|
||||||
|
param_categories={
|
||||||
|
"connectionId": "body",
|
||||||
|
"datasourceNamePrefix": "body",
|
||||||
|
"enterpriseId": "body",
|
||||||
|
"status": "body",
|
||||||
|
"remark": "body",
|
||||||
|
"syncTables": "body",
|
||||||
|
"databases": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
BATCH_UPDATE_DATASOURCE_CONFIGS = _tool(
|
||||||
|
name="batch_update_datasource_configs",
|
||||||
|
description="批量修改数据源配置并重新同步表结构",
|
||||||
|
method="PUT",
|
||||||
|
path="/datasource/config/batch",
|
||||||
|
properties={
|
||||||
|
"status": {"type": "integer", "description": "状态"},
|
||||||
|
"remark": {"type": "string", "description": "批量更新备注"},
|
||||||
|
"syncTables": {"type": "boolean", "description": "是否同步表结构"},
|
||||||
|
"datasources": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "要更新的数据源配置列表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "integer"},
|
||||||
|
"datasourceName": {"type": "string"},
|
||||||
|
"tableNames": {"type": "array", "items": {"type": "string"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required=["datasources"],
|
||||||
|
param_categories={
|
||||||
|
"status": "body",
|
||||||
|
"remark": "body",
|
||||||
|
"syncTables": "body",
|
||||||
|
"datasources": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
DELETE_DATASOURCE_CONFIGS = _tool(
|
||||||
|
name="delete_datasource_configs",
|
||||||
|
description="批量删除数据源配置",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/config/deletes",
|
||||||
|
properties={
|
||||||
|
"ids": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "要删除的数据源配置ID列表",
|
||||||
|
"items": {"type": "integer"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required=["ids"],
|
||||||
|
param_categories={"ids": "body"},
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_CONNECTION_CONFIG = _tool(
|
||||||
|
name="test_connection_config",
|
||||||
|
description="测试数据库连接是否正常",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/config/testConnectionConfig",
|
||||||
|
properties={
|
||||||
|
"host": {"type": "string", "description": "主机地址"},
|
||||||
|
"port": {"type": "integer", "description": "端口"},
|
||||||
|
"username": {"type": "string", "description": "用户名"},
|
||||||
|
"password": {"type": "string", "description": "密码"},
|
||||||
|
"datasourceType": {"type": "string", "description": "数据库类型,例如 PostgreSQL(默认 PostgreSQL)", "default": "PostgreSQL"},
|
||||||
|
},
|
||||||
|
required=["host", "port", "username", "password"],
|
||||||
|
param_categories={
|
||||||
|
"host": "body",
|
||||||
|
"port": "body",
|
||||||
|
"username": "body",
|
||||||
|
"password": "body",
|
||||||
|
"datasourceType": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
CHANGE_DATASOURCE_STATUS = _tool(
|
||||||
|
name="change_datasource_status",
|
||||||
|
description="修改数据源配置的启用/禁用状态",
|
||||||
|
method="PUT",
|
||||||
|
path="/datasource/config/changeStatus",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "数据源配置ID"},
|
||||||
|
"status": {"type": "integer", "description": "状态(0正常/1停用)"},
|
||||||
|
},
|
||||||
|
required=["id", "status"],
|
||||||
|
param_categories={"id": "body", "status": "body"},
|
||||||
|
)
|
||||||
|
|
||||||
|
EXPORT_DATASOURCE_CONFIGS = _tool(
|
||||||
|
name="export_datasource_configs",
|
||||||
|
description="导出数据源配置列表为Excel文件",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/config/export",
|
||||||
|
properties={
|
||||||
|
"datasourceName": {"type": "string", "description": "数据源名称(模糊查询)"},
|
||||||
|
"datasourceId": {"type": "integer", "description": "连接ID"},
|
||||||
|
"datasourceType": {"type": "string", "description": "数据库类型"},
|
||||||
|
"status": {"type": "integer", "description": "状态"},
|
||||||
|
},
|
||||||
|
required=[],
|
||||||
|
param_categories={
|
||||||
|
"datasourceName": "query",
|
||||||
|
"datasourceId": "query",
|
||||||
|
"datasourceType": "query",
|
||||||
|
"status": "query",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# =============================================================================
|
||||||
|
# 2. 数据库连接实例管理
|
||||||
|
# =============================================================================
|
||||||
|
|
||||||
|
LIST_CONNECTIONS = _tool(
|
||||||
|
name="list_connections",
|
||||||
|
description="查询数据库连接实例列表,支持分页和筛选",
|
||||||
|
method="GET",
|
||||||
|
path="/datasource/connection/list",
|
||||||
|
properties={
|
||||||
|
"datasourceName": {"type": "string", "description": "数据源名称(模糊查询)"},
|
||||||
|
"status": {"type": "integer", "description": "状态"},
|
||||||
|
"testStatus": {"type": "integer", "description": "测试状态(0未测试/1成功/2失败)"},
|
||||||
|
"sourceType": {"type": "string", "description": "连接来源(builtin/external)"},
|
||||||
|
**_PAGE_PROPS,
|
||||||
|
},
|
||||||
|
required=[],
|
||||||
|
param_categories={
|
||||||
|
"datasourceName": "query",
|
||||||
|
"status": "query",
|
||||||
|
"testStatus": "query",
|
||||||
|
"sourceType": "query",
|
||||||
|
"pageNum": "query",
|
||||||
|
"pageSize": "query",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
GET_CONNECTION = _tool(
|
||||||
|
name="get_connection",
|
||||||
|
description="获取指定连接实例的详细信息",
|
||||||
|
method="GET",
|
||||||
|
path="/datasource/connection/{id}",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
},
|
||||||
|
required=["id"],
|
||||||
|
param_categories={"id": "path"},
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE_CONNECTION = _tool(
|
||||||
|
name="create_connection",
|
||||||
|
description="创建新的数据库连接实例",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection",
|
||||||
|
properties={
|
||||||
|
"datasourceName": {"type": "string", "description": "连接名称"},
|
||||||
|
"datasourceType": {"type": "string", "description": "数据库类型,例如 PostgreSQL(默认 PostgreSQL)", "default": "PostgreSQL"},
|
||||||
|
"connectionType": {"type": "string", "description": "连接方式,例如 user_password(默认 user_password)", "default": "user_password"},
|
||||||
|
"host": {"type": "string", "description": "主机地址"},
|
||||||
|
"port": {"type": "integer", "description": "端口"},
|
||||||
|
"username": {"type": "string", "description": "用户名"},
|
||||||
|
"password": {"type": "string", "description": "密码"},
|
||||||
|
"status": {"type": "integer", "description": "状态"},
|
||||||
|
"remark": {"type": "string", "description": "备注信息"},
|
||||||
|
},
|
||||||
|
required=["datasourceName", "host", "port", "username", "password"],
|
||||||
|
param_categories={
|
||||||
|
"datasourceName": "body",
|
||||||
|
"datasourceType": "body",
|
||||||
|
"connectionType": "body",
|
||||||
|
"host": "body",
|
||||||
|
"port": "body",
|
||||||
|
"username": "body",
|
||||||
|
"password": "body",
|
||||||
|
"status": "body",
|
||||||
|
"remark": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
UPDATE_CONNECTION = _tool(
|
||||||
|
name="update_connection",
|
||||||
|
description="修改数据库连接实例信息",
|
||||||
|
method="PUT",
|
||||||
|
path="/datasource/connection",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"datasourceName": {"type": "string", "description": "连接名称"},
|
||||||
|
"host": {"type": "string", "description": "主机地址"},
|
||||||
|
"port": {"type": "integer", "description": "端口"},
|
||||||
|
"username": {"type": "string", "description": "用户名"},
|
||||||
|
"password": {"type": "string", "description": "密码"},
|
||||||
|
"remark": {"type": "string", "description": "更新备注"},
|
||||||
|
},
|
||||||
|
required=["id"],
|
||||||
|
param_categories={
|
||||||
|
"id": "body",
|
||||||
|
"datasourceName": "body",
|
||||||
|
"host": "body",
|
||||||
|
"port": "body",
|
||||||
|
"username": "body",
|
||||||
|
"password": "body",
|
||||||
|
"remark": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
DELETE_CONNECTION = _tool(
|
||||||
|
name="delete_connection",
|
||||||
|
description="删除指定的连接实例",
|
||||||
|
method="DELETE",
|
||||||
|
path="/datasource/connection/{id}",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
},
|
||||||
|
required=["id"],
|
||||||
|
param_categories={"id": "path"},
|
||||||
|
)
|
||||||
|
|
||||||
|
TEST_CONNECTION = _tool(
|
||||||
|
name="test_connection",
|
||||||
|
description="测试数据库连接是否可用",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/test",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"host": {"type": "string", "description": "主机地址"},
|
||||||
|
"port": {"type": "integer", "description": "端口"},
|
||||||
|
"username": {"type": "string", "description": "用户名"},
|
||||||
|
"password": {"type": "string", "description": "密码"},
|
||||||
|
"datasourceType": {"type": "string", "description": "数据库类型,例如 PostgreSQL(默认 PostgreSQL)", "default": "PostgreSQL"},
|
||||||
|
},
|
||||||
|
required=["host", "port", "username", "password"],
|
||||||
|
param_categories={
|
||||||
|
"id": "body",
|
||||||
|
"host": "body",
|
||||||
|
"port": "body",
|
||||||
|
"username": "body",
|
||||||
|
"password": "body",
|
||||||
|
"datasourceType": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
CHANGE_CONNECTION_STATUS = _tool(
|
||||||
|
name="change_connection_status",
|
||||||
|
description="修改连接实例的启用/禁用状态",
|
||||||
|
method="PUT",
|
||||||
|
path="/datasource/connection/changeStatus",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"status": {"type": "integer", "description": "状态(0正常/1停用)"},
|
||||||
|
},
|
||||||
|
required=["id", "status"],
|
||||||
|
param_categories={"id": "body", "status": "body"},
|
||||||
|
)
|
||||||
|
|
||||||
|
REALTIME_STRUCTURE = _tool(
|
||||||
|
name="realtime_structure",
|
||||||
|
description="实时查询连接下的所有数据库和表结构(直接连接数据库服务器查询)",
|
||||||
|
method="GET",
|
||||||
|
path="/datasource/connection/realtime/structure/{id}",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
},
|
||||||
|
required=["id"],
|
||||||
|
param_categories={"id": "path"},
|
||||||
|
)
|
||||||
|
|
||||||
|
REALTIME_DATABASES = _tool(
|
||||||
|
name="realtime_databases",
|
||||||
|
description="实时查询连接下的所有数据库名称",
|
||||||
|
method="GET",
|
||||||
|
path="/datasource/connection/realtime/databases/{id}",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
},
|
||||||
|
required=["id"],
|
||||||
|
param_categories={"id": "path"},
|
||||||
|
)
|
||||||
|
|
||||||
|
REALTIME_TABLES = _tool(
|
||||||
|
name="realtime_tables",
|
||||||
|
description="实时查询指定数据库下的所有表",
|
||||||
|
method="GET",
|
||||||
|
path="/datasource/connection/realtime/tables/{id}",
|
||||||
|
properties={
|
||||||
|
"id": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||||
|
},
|
||||||
|
required=["id", "databaseName"],
|
||||||
|
param_categories={"id": "path", "databaseName": "query"},
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE_BUILTIN_POSTGRESQL = _tool(
|
||||||
|
name="create_builtin_postgresql",
|
||||||
|
description="创建内置 PostgreSQL 数据库连接(使用配置文件中的连接信息)",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/create_builtin_postgresql",
|
||||||
|
properties={
|
||||||
|
"datasourceName": {"type": "string", "description": "连接名称"},
|
||||||
|
"remark": {"type": "string", "description": "备注"},
|
||||||
|
},
|
||||||
|
required=["datasourceName"],
|
||||||
|
param_categories={"datasourceName": "body", "remark": "body"},
|
||||||
|
)
|
||||||
|
|
||||||
|
UPDATE_BUILTIN_DATABASE = _tool(
|
||||||
|
name="update_builtin_database",
|
||||||
|
description="修改内置 PostgreSQL 连接信息",
|
||||||
|
method="PUT",
|
||||||
|
path="/datasource/connection/update_builtin_database",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"datasourceName": {"type": "string", "description": "新名称"},
|
||||||
|
"remark": {"type": "string", "description": "更新备注"},
|
||||||
|
},
|
||||||
|
required=["connectionId"],
|
||||||
|
param_categories={"connectionId": "body", "datasourceName": "body", "remark": "body"},
|
||||||
|
)
|
||||||
|
|
||||||
|
EXECUTE_SQL = _tool(
|
||||||
|
name="execute_sql",
|
||||||
|
description="在指定数据源上执行 SQL 语句,支持参数化查询和环境切换",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/{datasourceId}/execute_sql",
|
||||||
|
properties={
|
||||||
|
"datasourceId": {"type": "integer", "description": "数据源配置ID"},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "目标环境(prod/test,默认 prod)",
|
||||||
|
"default": "prod",
|
||||||
|
},
|
||||||
|
"sql": {"type": "string", "description": "SQL 语句,使用 ? 占位符"},
|
||||||
|
"params": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "SQL 参数列表",
|
||||||
|
"items": {},
|
||||||
|
},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||||
|
**_API_KEY_PROP,
|
||||||
|
},
|
||||||
|
required=["datasourceId", "sql"],
|
||||||
|
param_categories={
|
||||||
|
"datasourceId": "path",
|
||||||
|
"target": "query",
|
||||||
|
"sql": "body",
|
||||||
|
"params": "body",
|
||||||
|
"databaseName": "body",
|
||||||
|
"apiKey": "header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE_DATABASE = _tool(
|
||||||
|
name="create_database",
|
||||||
|
description="在指定连接上创建新数据库(目前仅支持 PostgreSQL)",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/{connectionId}/create_database",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||||
|
"encoding": {"type": "string", "description": "字符编码,例如 UTF8"},
|
||||||
|
"owner": {"type": "string", "description": "所有者"},
|
||||||
|
**_API_KEY_PROP,
|
||||||
|
},
|
||||||
|
required=["connectionId", "databaseName"],
|
||||||
|
param_categories={
|
||||||
|
"connectionId": "path",
|
||||||
|
"databaseName": "body",
|
||||||
|
"encoding": "body",
|
||||||
|
"owner": "body",
|
||||||
|
"apiKey": "header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE_TABLE = _tool(
|
||||||
|
name="create_table",
|
||||||
|
description="在指定数据库中创建新表",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/{connectionId}/create_table",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||||
|
"tableName": {"type": "string", "description": "表名"},
|
||||||
|
"tableComment": {"type": "string", "description": "表注释"},
|
||||||
|
"columns": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "列定义列表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"columnName": {"type": "string"},
|
||||||
|
"columnType": {"type": "string"},
|
||||||
|
"columnLength": {"type": "integer"},
|
||||||
|
"isPrimaryKey": {"type": "boolean"},
|
||||||
|
"isNullable": {"type": "boolean"},
|
||||||
|
"columnComment": {"type": "string"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
**_API_KEY_PROP,
|
||||||
|
},
|
||||||
|
required=["connectionId", "databaseName", "tableName", "columns"],
|
||||||
|
param_categories={
|
||||||
|
"connectionId": "path",
|
||||||
|
"databaseName": "body",
|
||||||
|
"tableName": "body",
|
||||||
|
"tableComment": "body",
|
||||||
|
"columns": "body",
|
||||||
|
"apiKey": "header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
CREATE_DATABASE_TABLE = _tool(
|
||||||
|
name="create_database_table",
|
||||||
|
description="同时创建数据库和表(一次性操作)",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/{connectionId}/create_database_table",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||||
|
"encoding": {"type": "string", "description": "字符编码"},
|
||||||
|
"owner": {"type": "string", "description": "所有者"},
|
||||||
|
"tables": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "要创建的表列表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"tableName": {"type": "string"},
|
||||||
|
"tableComment": {"type": "string"},
|
||||||
|
"columns": {"type": "array"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
**_API_KEY_PROP,
|
||||||
|
},
|
||||||
|
required=["connectionId", "databaseName", "tables"],
|
||||||
|
param_categories={
|
||||||
|
"connectionId": "path",
|
||||||
|
"databaseName": "body",
|
||||||
|
"encoding": "body",
|
||||||
|
"owner": "body",
|
||||||
|
"tables": "body",
|
||||||
|
"apiKey": "header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ALTER_DATABASE = _tool(
|
||||||
|
name="alter_database",
|
||||||
|
description="修改数据库属性(重命名、更改所有者、更改编码)",
|
||||||
|
method="PUT",
|
||||||
|
path="/datasource/connection/{connectionId}/alter_database",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "原数据库名称"},
|
||||||
|
"newName": {"type": "string", "description": "新数据库名称"},
|
||||||
|
"newOwner": {"type": "string", "description": "新所有者"},
|
||||||
|
"newEncoding": {"type": "string", "description": "新字符编码"},
|
||||||
|
**_API_KEY_PROP,
|
||||||
|
},
|
||||||
|
required=["connectionId", "databaseName"],
|
||||||
|
param_categories={
|
||||||
|
"connectionId": "path",
|
||||||
|
"databaseName": "body",
|
||||||
|
"newName": "body",
|
||||||
|
"newOwner": "body",
|
||||||
|
"newEncoding": "body",
|
||||||
|
"apiKey": "header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ALTER_TABLE = _tool(
|
||||||
|
name="alter_table",
|
||||||
|
description="修改表结构(添加列、删除列、重命名列、修改列类型等)",
|
||||||
|
method="PUT",
|
||||||
|
path="/datasource/connection/{connectionId}/alter_table",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||||
|
"tableName": {"type": "string", "description": "表名"},
|
||||||
|
"tableComment": {"type": "string", "description": "表注释"},
|
||||||
|
"operations": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "操作列表",
|
||||||
|
"items": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"operation": {"type": "string"},
|
||||||
|
"column": {"type": "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
**_API_KEY_PROP,
|
||||||
|
},
|
||||||
|
required=["connectionId", "databaseName", "tableName", "operations"],
|
||||||
|
param_categories={
|
||||||
|
"connectionId": "path",
|
||||||
|
"databaseName": "body",
|
||||||
|
"tableName": "body",
|
||||||
|
"tableComment": "body",
|
||||||
|
"operations": "body",
|
||||||
|
"apiKey": "header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
GENERATE_TABLE = _tool(
|
||||||
|
name="generate_table",
|
||||||
|
description="使用 AI 根据需求描述生成表结构(异步任务)",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/generate_table",
|
||||||
|
properties={
|
||||||
|
"requirement": {"type": "string", "description": "需求描述"},
|
||||||
|
"databaseId": {"type": "integer", "description": "关联的数据库ID"},
|
||||||
|
},
|
||||||
|
required=["requirement"],
|
||||||
|
param_categories={"requirement": "body", "databaseId": "body"},
|
||||||
|
)
|
||||||
|
|
||||||
|
IMPORT_DOCUMENT_PREVIEW = _tool(
|
||||||
|
name="import_document_preview",
|
||||||
|
description="上传 Excel/CSV 文件,AI 识别表结构并预览前 10 条数据",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/{connectionId}/import_document/preview",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "目标环境(prod/test,默认 prod)",
|
||||||
|
"default": "prod",
|
||||||
|
},
|
||||||
|
"filePath": {"type": "string", "description": "本地 Excel/CSV 文件路径"},
|
||||||
|
},
|
||||||
|
required=["connectionId", "filePath"],
|
||||||
|
param_categories={
|
||||||
|
"connectionId": "path",
|
||||||
|
"target": "query",
|
||||||
|
"filePath": "file",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
IMPORT_DOCUMENT_CONFIRM = _tool(
|
||||||
|
name="import_document_confirm",
|
||||||
|
description="确认导入,创建表并插入数据",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/{connectionId}/import_document/confirm",
|
||||||
|
properties={
|
||||||
|
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "目标环境(prod/test,默认 prod)",
|
||||||
|
"default": "prod",
|
||||||
|
},
|
||||||
|
"tableStructure": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "表结构定义",
|
||||||
|
},
|
||||||
|
"allData": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "完整导入数据",
|
||||||
|
},
|
||||||
|
**_API_KEY_PROP,
|
||||||
|
},
|
||||||
|
required=["connectionId", "tableStructure", "allData"],
|
||||||
|
param_categories={
|
||||||
|
"connectionId": "path",
|
||||||
|
"target": "query",
|
||||||
|
"tableStructure": "body",
|
||||||
|
"allData": "body",
|
||||||
|
"apiKey": "header",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
BUILTIN_TABLE_DATA = _tool(
|
||||||
|
name="builtin_table_data",
|
||||||
|
description="根据表ID查询表结构和数据(分页)",
|
||||||
|
method="GET",
|
||||||
|
path="/datasource/connection/builtin/table/{tableId}",
|
||||||
|
properties={
|
||||||
|
"tableId": {"type": "integer", "description": "表ID"},
|
||||||
|
"pageNum": {"type": "integer", "description": "页码(默认1)"},
|
||||||
|
"pageSize": {"type": "integer", "description": "每页数量(默认100)"},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "目标环境(prod/test,默认 prod)",
|
||||||
|
"default": "prod",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required=["tableId"],
|
||||||
|
param_categories={
|
||||||
|
"tableId": "path",
|
||||||
|
"pageNum": "query",
|
||||||
|
"pageSize": "query",
|
||||||
|
"target": "query",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
BUILTIN_TABLE_INSERT = _tool(
|
||||||
|
name="builtin_table_insert",
|
||||||
|
description="向指定表插入一条数据",
|
||||||
|
method="POST",
|
||||||
|
path="/datasource/connection/builtin/table/{tableId}/rows",
|
||||||
|
properties={
|
||||||
|
"tableId": {"type": "integer", "description": "表ID"},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "目标环境(prod/test,默认 prod)",
|
||||||
|
"default": "prod",
|
||||||
|
},
|
||||||
|
"data": {"type": "object", "description": "要插入的数据"},
|
||||||
|
},
|
||||||
|
required=["tableId", "data"],
|
||||||
|
param_categories={
|
||||||
|
"tableId": "path",
|
||||||
|
"target": "query",
|
||||||
|
"data": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
BUILTIN_TABLE_UPDATE = _tool(
|
||||||
|
name="builtin_table_update",
|
||||||
|
description="根据主键更新表数据",
|
||||||
|
method="PUT",
|
||||||
|
path="/datasource/connection/builtin/table/{tableId}/rows",
|
||||||
|
properties={
|
||||||
|
"tableId": {"type": "integer", "description": "表ID"},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "目标环境(prod/test,默认 prod)",
|
||||||
|
"default": "prod",
|
||||||
|
},
|
||||||
|
"data": {"type": "object", "description": "要更新的数据"},
|
||||||
|
"primaryKey": {"type": "object", "description": "主键条件"},
|
||||||
|
},
|
||||||
|
required=["tableId", "data", "primaryKey"],
|
||||||
|
param_categories={
|
||||||
|
"tableId": "path",
|
||||||
|
"target": "query",
|
||||||
|
"data": "body",
|
||||||
|
"primaryKey": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
BUILTIN_TABLE_DELETE = _tool(
|
||||||
|
name="builtin_table_delete",
|
||||||
|
description="根据主键批量删除表数据",
|
||||||
|
method="DELETE",
|
||||||
|
path="/datasource/connection/builtin/table/{tableId}/rows",
|
||||||
|
properties={
|
||||||
|
"tableId": {"type": "integer", "description": "表ID"},
|
||||||
|
"target": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "目标环境(prod/test,默认 prod)",
|
||||||
|
"default": "prod",
|
||||||
|
},
|
||||||
|
"primaryKeys": {
|
||||||
|
"type": "array",
|
||||||
|
"description": "主键条件列表",
|
||||||
|
"items": {"type": "object"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required=["tableId", "primaryKeys"],
|
||||||
|
param_categories={
|
||||||
|
"tableId": "path",
|
||||||
|
"target": "query",
|
||||||
|
"primaryKeys": "body",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ALL_TOOLS = [
|
||||||
|
LIST_DATASOURCE_CONFIGS,
|
||||||
|
GET_DATASOURCE_CONFIG,
|
||||||
|
BATCH_CREATE_DATASOURCE_CONFIGS,
|
||||||
|
REPLACE_DATASOURCE_CONFIGS,
|
||||||
|
BATCH_UPDATE_DATASOURCE_CONFIGS,
|
||||||
|
DELETE_DATASOURCE_CONFIGS,
|
||||||
|
TEST_CONNECTION_CONFIG,
|
||||||
|
CHANGE_DATASOURCE_STATUS,
|
||||||
|
EXPORT_DATASOURCE_CONFIGS,
|
||||||
|
LIST_CONNECTIONS,
|
||||||
|
GET_CONNECTION,
|
||||||
|
CREATE_CONNECTION,
|
||||||
|
UPDATE_CONNECTION,
|
||||||
|
DELETE_CONNECTION,
|
||||||
|
TEST_CONNECTION,
|
||||||
|
CHANGE_CONNECTION_STATUS,
|
||||||
|
REALTIME_STRUCTURE,
|
||||||
|
REALTIME_DATABASES,
|
||||||
|
REALTIME_TABLES,
|
||||||
|
CREATE_BUILTIN_POSTGRESQL,
|
||||||
|
UPDATE_BUILTIN_DATABASE,
|
||||||
|
EXECUTE_SQL,
|
||||||
|
CREATE_DATABASE,
|
||||||
|
CREATE_TABLE,
|
||||||
|
CREATE_DATABASE_TABLE,
|
||||||
|
ALTER_DATABASE,
|
||||||
|
ALTER_TABLE,
|
||||||
|
GENERATE_TABLE,
|
||||||
|
IMPORT_DOCUMENT_PREVIEW,
|
||||||
|
IMPORT_DOCUMENT_CONFIRM,
|
||||||
|
BUILTIN_TABLE_DATA,
|
||||||
|
BUILTIN_TABLE_INSERT,
|
||||||
|
BUILTIN_TABLE_UPDATE,
|
||||||
|
BUILTIN_TABLE_DELETE,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
TOOL_MAP = {tool["name"]: tool for tool in ALL_TOOLS}
|
||||||
|
|
||||||
|
|
||||||
|
def list_tools() -> List[Dict[str, Any]]:
|
||||||
|
"""Return all tool definitions."""
|
||||||
|
return ALL_TOOLS
|
||||||
|
|
||||||
|
|
||||||
|
def get_tool(name: str) -> Dict[str, Any]:
|
||||||
|
"""Return a tool definition by name."""
|
||||||
|
return TOOL_MAP.get(name)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
"""Utils package for lzwcai_mcp_agile_db_third."""
|
||||||
|
|
||||||
|
from .env_config import get_backend_base_url, get_api_key, get_env_config
|
||||||
|
from .api_client import DataSourceAPIClient, api_request
|
||||||
|
from .logger_config import logger_config
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"get_backend_base_url",
|
||||||
|
"get_api_key",
|
||||||
|
"get_env_config",
|
||||||
|
"DataSourceAPIClient",
|
||||||
|
"api_request",
|
||||||
|
"logger_config",
|
||||||
|
]
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
"""Backend API client for datasource APIs."""
|
||||||
|
|
||||||
|
import base64
|
||||||
|
import logging
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
from typing import Any, Dict, Optional
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
|
import httpx
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .env_config import get_backend_base_url
|
||||||
|
except ImportError:
|
||||||
|
from env_config import get_backend_base_url
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
DEFAULT_TOKEN = (
|
||||||
|
""
|
||||||
|
)
|
||||||
|
|
||||||
|
_SENSITIVE_FIELDS = {"password", "apiKey", "token", "secret"}
|
||||||
|
|
||||||
|
|
||||||
|
def _mask_secret(value: Optional[str]) -> str:
|
||||||
|
"""Mask a secret for logging, keeping only a few leading/trailing chars."""
|
||||||
|
if not value:
|
||||||
|
return "<空>"
|
||||||
|
if len(value) <= 8:
|
||||||
|
return "***"
|
||||||
|
return f"{value[:4]}...{value[-4:]} (len={len(value)})"
|
||||||
|
|
||||||
|
|
||||||
|
class DataSourceAPIClient:
|
||||||
|
"""HTTP client for backend datasource APIs."""
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
token: Optional[str] = None,
|
||||||
|
):
|
||||||
|
self.base_url = (base_url or get_backend_base_url()).rstrip("/")
|
||||||
|
# Prefer explicit token, then API_KEY env var, then built-in default.
|
||||||
|
resolved = token or os.environ.get("API_KEY") or DEFAULT_TOKEN
|
||||||
|
self.token = resolved.removeprefix("Bearer ").strip()
|
||||||
|
self.client = httpx.Client(timeout=120.0)
|
||||||
|
|
||||||
|
def close(self) -> None:
|
||||||
|
"""Close the underlying HTTP client."""
|
||||||
|
self.client.close()
|
||||||
|
|
||||||
|
def _get_headers(self, api_key: Optional[str] = None) -> Dict[str, str]:
|
||||||
|
headers = {
|
||||||
|
"Authorization": f"Bearer {self.token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
}
|
||||||
|
if api_key:
|
||||||
|
headers["X-Datasource-API-Key"] = api_key
|
||||||
|
return headers
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _sanitize_value(value: Any) -> Any:
|
||||||
|
"""Redact sensitive nested values for logging."""
|
||||||
|
if isinstance(value, dict):
|
||||||
|
return {
|
||||||
|
k: "***" if k in _SENSITIVE_FIELDS else DataSourceAPIClient._sanitize_value(v)
|
||||||
|
for k, v in value.items()
|
||||||
|
}
|
||||||
|
if isinstance(value, list):
|
||||||
|
return [DataSourceAPIClient._sanitize_value(item) for item in value]
|
||||||
|
return value
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _file_tuple(file_path: str) -> tuple:
|
||||||
|
"""Build a multipart file tuple with filename and content type."""
|
||||||
|
filename = os.path.basename(file_path)
|
||||||
|
content_type, _ = mimetypes.guess_type(filename)
|
||||||
|
if content_type is None:
|
||||||
|
content_type = "application/octet-stream"
|
||||||
|
return (filename, open(file_path, "rb"), content_type)
|
||||||
|
|
||||||
|
def request(
|
||||||
|
self,
|
||||||
|
method: str,
|
||||||
|
path_template: str,
|
||||||
|
path_params: Optional[Dict[str, Any]] = None,
|
||||||
|
query_params: Optional[Dict[str, Any]] = None,
|
||||||
|
body: Optional[Dict[str, Any]] = None,
|
||||||
|
file_path: Optional[str] = None,
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Send a request and return JSON response."""
|
||||||
|
path_params = path_params or {}
|
||||||
|
query_params = query_params or {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
url = self._build_url(path_template, path_params, query_params)
|
||||||
|
headers = self._get_headers(api_key)
|
||||||
|
|
||||||
|
safe_body = self._sanitize_value(body) if body else body
|
||||||
|
logger.info(f"调用后端 API: {method} {url}")
|
||||||
|
logger.info(
|
||||||
|
f"请求密钥 - X-Datasource-API-Key: {_mask_secret(api_key)}, "
|
||||||
|
f"Authorization token: {_mask_secret(self.token)}"
|
||||||
|
)
|
||||||
|
logger.debug(
|
||||||
|
f"path_params={path_params}, query_params={query_params}, "
|
||||||
|
f"body={safe_body}, file_path={file_path}"
|
||||||
|
)
|
||||||
|
|
||||||
|
method = method.upper()
|
||||||
|
if method == "GET":
|
||||||
|
response = self.client.get(url, headers=headers)
|
||||||
|
elif method == "DELETE":
|
||||||
|
if body is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
response = self.client.request(method, url, headers=headers, json=body)
|
||||||
|
else:
|
||||||
|
response = self.client.delete(url, headers=headers)
|
||||||
|
elif file_path:
|
||||||
|
# Multipart upload
|
||||||
|
headers.pop("Content-Type", None)
|
||||||
|
file_tuple = self._file_tuple(file_path)
|
||||||
|
files = {"file": file_tuple}
|
||||||
|
try:
|
||||||
|
response = self.client.request(
|
||||||
|
method, url, headers=headers, data=body, files=files
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
file_tuple[1].close()
|
||||||
|
elif body is not None:
|
||||||
|
headers["Content-Type"] = "application/json"
|
||||||
|
response = self.client.request(method, url, headers=headers, json=body)
|
||||||
|
else:
|
||||||
|
response = self.client.request(method, url, headers=headers)
|
||||||
|
|
||||||
|
response.raise_for_status()
|
||||||
|
content_type = response.headers.get("content-type", "")
|
||||||
|
if "application/json" in content_type:
|
||||||
|
data = response.json()
|
||||||
|
else:
|
||||||
|
# Non-JSON response (e.g., Excel export), encode as base64
|
||||||
|
data = {
|
||||||
|
"success": True,
|
||||||
|
"contentType": content_type,
|
||||||
|
"fileBase64": base64.b64encode(response.content).decode("ascii"),
|
||||||
|
"message": "后端返回非 JSON 内容,已使用 base64 编码",
|
||||||
|
}
|
||||||
|
logger.info(f"后端 API 调用成功: {method} {url}")
|
||||||
|
logger.debug(f"响应: {data}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
except httpx.TimeoutException as e:
|
||||||
|
error_msg = f"API 请求超时: {method} {path_template}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise Exception(error_msg) from e
|
||||||
|
except httpx.HTTPStatusError as e:
|
||||||
|
error_msg = f"API 请求失败 (HTTP {e.response.status_code}): {e.response.text}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise Exception(error_msg) from e
|
||||||
|
except httpx.RequestError as e:
|
||||||
|
error_msg = f"API 请求异常: {method} {path_template}, 错误: {e}"
|
||||||
|
logger.error(error_msg)
|
||||||
|
raise Exception(error_msg) from e
|
||||||
|
except Exception as e:
|
||||||
|
error_msg = f"处理 API 响应时出错: {e}"
|
||||||
|
logger.error(error_msg, exc_info=True)
|
||||||
|
raise Exception(error_msg) from e
|
||||||
|
|
||||||
|
def _build_url(
|
||||||
|
self,
|
||||||
|
path_template: str,
|
||||||
|
path_params: Dict[str, Any],
|
||||||
|
query_params: Dict[str, Any],
|
||||||
|
) -> str:
|
||||||
|
path = path_template
|
||||||
|
for key, value in path_params.items():
|
||||||
|
path = path.replace(f"{{{key}}}", str(value))
|
||||||
|
|
||||||
|
url = f"{self.base_url}{path}"
|
||||||
|
|
||||||
|
if query_params:
|
||||||
|
filtered = {k: v for k, v in query_params.items() if v is not None}
|
||||||
|
if filtered:
|
||||||
|
url = f"{url}?{urlencode(filtered)}"
|
||||||
|
|
||||||
|
return url
|
||||||
|
|
||||||
|
|
||||||
|
def api_request(
|
||||||
|
method: str,
|
||||||
|
path_template: str,
|
||||||
|
path_params: Optional[Dict[str, Any]] = None,
|
||||||
|
query_params: Optional[Dict[str, Any]] = None,
|
||||||
|
body: Optional[Dict[str, Any]] = None,
|
||||||
|
file_path: Optional[str] = None,
|
||||||
|
api_key: Optional[str] = None,
|
||||||
|
base_url: Optional[str] = None,
|
||||||
|
token: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Convenience wrapper for one-off API requests."""
|
||||||
|
client = DataSourceAPIClient(base_url=base_url, token=token)
|
||||||
|
try:
|
||||||
|
return client.request(
|
||||||
|
method=method,
|
||||||
|
path_template=path_template,
|
||||||
|
path_params=path_params,
|
||||||
|
query_params=query_params,
|
||||||
|
body=body,
|
||||||
|
file_path=file_path,
|
||||||
|
api_key=api_key,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
client.close()
|
||||||
|
|
||||||
|
|
||||||
|
# Module-level default client
|
||||||
|
_default_client = DataSourceAPIClient()
|
||||||
|
|
||||||
|
|
||||||
|
def get_default_client() -> DataSourceAPIClient:
|
||||||
|
return _default_client
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
"""环境变量配置模块"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def get_backend_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str:
|
||||||
|
"""
|
||||||
|
获取后端 API 基础 URL。
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
backendBaseUrl: 后端 API 基础 URL
|
||||||
|
"""
|
||||||
|
return os.environ.get("backendBaseUrl", default)
|
||||||
|
|
||||||
|
|
||||||
|
def get_api_key(default: str = "") -> str:
|
||||||
|
"""
|
||||||
|
获取默认 API 密钥(X-Datasource-API-Key)。
|
||||||
|
|
||||||
|
Environment Variables:
|
||||||
|
datasourceApiKey: 默认 API 密钥
|
||||||
|
"""
|
||||||
|
return os.environ.get("datasourceApiKey", default)
|
||||||
|
|
||||||
|
|
||||||
|
def get_env_config() -> dict:
|
||||||
|
"""获取所有环境配置。"""
|
||||||
|
return {
|
||||||
|
"backend_base_url": get_backend_base_url(),
|
||||||
|
"api_key": get_api_key(),
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
"""统一日志配置模块"""
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class LoggerConfig:
|
||||||
|
"""日志配置管理类"""
|
||||||
|
|
||||||
|
def __init__(self, logs_dir: str = None):
|
||||||
|
if logs_dir:
|
||||||
|
self.logs_dir = Path(logs_dir)
|
||||||
|
else:
|
||||||
|
project_root = Path(__file__).parent.parent
|
||||||
|
self.logs_dir = project_root / "logs"
|
||||||
|
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
self.log_format = "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s"
|
||||||
|
self.date_format = "%Y-%m-%d %H:%M:%S"
|
||||||
|
self._initialized = False
|
||||||
|
|
||||||
|
def setup_logging(
|
||||||
|
self,
|
||||||
|
app_name: str = "lzwcai_mcp_agile_db_third",
|
||||||
|
log_level: int = logging.INFO,
|
||||||
|
console_output: bool = False,
|
||||||
|
) -> logging.Logger:
|
||||||
|
"""设置系统日志配置。"""
|
||||||
|
if self._initialized:
|
||||||
|
return logging.getLogger()
|
||||||
|
|
||||||
|
root_logger = logging.getLogger()
|
||||||
|
root_logger.setLevel(log_level)
|
||||||
|
|
||||||
|
for handler in root_logger.handlers[:]:
|
||||||
|
root_logger.removeHandler(handler)
|
||||||
|
|
||||||
|
formatter = logging.Formatter(self.log_format, self.date_format)
|
||||||
|
|
||||||
|
# 主日志文件
|
||||||
|
main_log_file = self.logs_dir / f"{app_name}.log"
|
||||||
|
file_handler = logging.FileHandler(main_log_file, encoding="utf-8")
|
||||||
|
file_handler.setLevel(log_level)
|
||||||
|
file_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(file_handler)
|
||||||
|
|
||||||
|
# 错误日志文件
|
||||||
|
error_log_file = self.logs_dir / f"{app_name}_error.log"
|
||||||
|
error_handler = logging.FileHandler(error_log_file, encoding="utf-8")
|
||||||
|
error_handler.setLevel(logging.ERROR)
|
||||||
|
error_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(error_handler)
|
||||||
|
|
||||||
|
# MCP 使用 stdio 时,控制台日志必须输出到 stderr
|
||||||
|
if console_output:
|
||||||
|
console_handler = logging.StreamHandler(sys.stderr)
|
||||||
|
console_handler.setLevel(log_level)
|
||||||
|
console_handler.setFormatter(formatter)
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
self._initialized = True
|
||||||
|
root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}")
|
||||||
|
return root_logger
|
||||||
|
|
||||||
|
def setup_mcp_logging(self) -> logging.Logger:
|
||||||
|
"""设置 MCP 专用日志。"""
|
||||||
|
return self.create_component_logger("mcp_services", "mcp_services.log", logging.DEBUG)
|
||||||
|
|
||||||
|
def create_component_logger(
|
||||||
|
self, component_name: str, log_file: str = None, log_level: int = None
|
||||||
|
) -> logging.Logger:
|
||||||
|
"""为特定组件创建独立日志器。"""
|
||||||
|
logger = logging.getLogger(component_name)
|
||||||
|
if log_file:
|
||||||
|
component_log_file = self.logs_dir / log_file
|
||||||
|
handler = logging.FileHandler(component_log_file, encoding="utf-8")
|
||||||
|
handler.setFormatter(logging.Formatter(self.log_format, self.date_format))
|
||||||
|
if log_level is not None:
|
||||||
|
handler.setLevel(log_level)
|
||||||
|
logger.addHandler(handler)
|
||||||
|
return logger
|
||||||
|
|
||||||
|
|
||||||
|
logger_config = LoggerConfig()
|
||||||
17
lzwcai_mcp_agile_db_third/main.py
Normal file
17
lzwcai_mcp_agile_db_third/main.py
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
"""
|
||||||
|
Repository-local launcher for lzwcai-mcp-sqlexecutor.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Keep local developer defaults without overriding explicit environment settings.
|
||||||
|
os.environ.setdefault("backendBaseUrl", "http://192.168.2.236:8088")
|
||||||
|
os.environ.setdefault("datasourceApiKey", "yBOHyhCSpExAoEleimSfhbRzsF6SDiPYdGGwowXG-Sk")
|
||||||
|
from lzwcai_mcp_agile_db_third.main import main
|
||||||
|
main()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
41
lzwcai_mcp_agile_db_third/pyproject.toml
Normal file
41
lzwcai_mcp_agile_db_third/pyproject.toml
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["hatchling"]
|
||||||
|
build-backend = "hatchling.build"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "lzwcai-mcp-agile-db-third"
|
||||||
|
version = "0.1.6"
|
||||||
|
description = "MCP server for Agile DB third-party datasource APIs"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
license = {text = "MIT"}
|
||||||
|
authors = [
|
||||||
|
{name = "lzwcai", email = "your-email@example.com"},
|
||||||
|
]
|
||||||
|
keywords = ["mcp", "datasource", "database", "agile-db"]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"httpx>=0.28.1",
|
||||||
|
"mcp[cli]>=1.10.1",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
lzwcai-mcp-agile-db-third = "lzwcai_mcp_agile_db_third.main:main"
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel]
|
||||||
|
packages = ["lzwcai_mcp_agile_db_third"]
|
||||||
|
|
||||||
|
[tool.hatch.build.targets.wheel.force-include]
|
||||||
|
|
||||||
|
[tool.hatch.build]
|
||||||
|
exclude = [
|
||||||
|
"lzwcai_mcp_agile_db_third/logs/**",
|
||||||
|
"**/.__pycache__/**",
|
||||||
|
"lzwcai_mcp_agile_db_third/.gitignore",
|
||||||
|
".venv/**",
|
||||||
|
]
|
||||||
418
lzwcai_mcp_agile_db_third/selftest_tools.py
Normal file
418
lzwcai_mcp_agile_db_third/selftest_tools.py
Normal file
@@ -0,0 +1,418 @@
|
|||||||
|
"""End-to-end self-test for all lzwcai_mcp_agile_db_third MCP tools.
|
||||||
|
|
||||||
|
Runs every tool through the real MCP handler (handle_call_tool) against the
|
||||||
|
backend configured below. Uses selftest_-prefixed throwaway resources and
|
||||||
|
cleans them up at the end. The destructive full-replace tool is skipped.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
import csv
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
os.environ.setdefault("backendBaseUrl", "http://192.168.2.236:8088")
|
||||||
|
# Login token (Authorization Bearer) shared with the first-party AgileDB MCP server.
|
||||||
|
os.environ.setdefault(
|
||||||
|
"API_KEY",
|
||||||
|
"Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6"
|
||||||
|
"ImNlMDAwYjA4LWU0YTYtNGM2MS1hNzJiLWI3NTlmNmY1N2Q4NCJ9.jiNmGQZfL4-nSIFrLuaCt7mT"
|
||||||
|
"5zj0FOojAVkLeHwPOroI5jBxodrCe1PSwGO1OHq5Ztb0tLEVZw2FFVj0OlTceQ",
|
||||||
|
)
|
||||||
|
# Optional X-Datasource-API-Key for datasource-level permission checks (if enforced).
|
||||||
|
os.environ.setdefault("datasourceApiKey", "Mggkz34Yk8cbjUvCvQ-qeooNRg62WhSwwtxUUV6e0Pg")
|
||||||
|
|
||||||
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
sys.path.insert(0, HERE)
|
||||||
|
|
||||||
|
from lzwcai_mcp_agile_db_third.main import handle_call_tool # noqa: E402
|
||||||
|
from lzwcai_mcp_agile_db_third.mock_db import start as start_mock_db, stop as stop_mock_db # noqa: E402
|
||||||
|
from lzwcai_mcp_agile_db_third.tools import ALL_TOOLS # noqa: E402
|
||||||
|
|
||||||
|
RESULTS = [] # (name, status, detail)
|
||||||
|
TESTED = set()
|
||||||
|
|
||||||
|
|
||||||
|
async def call(name, args=None):
|
||||||
|
"""Invoke a tool through the MCP handler and return parsed JSON."""
|
||||||
|
TESTED.add(name)
|
||||||
|
try:
|
||||||
|
resp = await handle_call_tool(name, args or {})
|
||||||
|
return json.loads(resp[0].text)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
return {"_exception": repr(e)}
|
||||||
|
|
||||||
|
|
||||||
|
def summarize(data):
|
||||||
|
"""Extract a short (ok, detail) from a backend response."""
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
return False, str(data)[:200]
|
||||||
|
if "_exception" in data:
|
||||||
|
return False, data["_exception"][:200]
|
||||||
|
if "error" in data:
|
||||||
|
return False, str(data["error"])[:200]
|
||||||
|
code = data.get("code")
|
||||||
|
msg = data.get("msg", "")
|
||||||
|
if code == 200:
|
||||||
|
return True, f"code=200 msg={msg}"
|
||||||
|
if code is not None:
|
||||||
|
return False, f"code={code} msg={msg}"
|
||||||
|
# list/paginated or already-unwrapped payloads
|
||||||
|
return True, json.dumps(data, ensure_ascii=False)[:160]
|
||||||
|
|
||||||
|
|
||||||
|
def record(name, data, mode="ok"):
|
||||||
|
"""Record a tool result.
|
||||||
|
|
||||||
|
mode="ok" -> PASS only when backend code==200
|
||||||
|
mode="roundtrip" -> PASS when the call round-trips (no exception and the
|
||||||
|
backend returned a structured response), regardless of
|
||||||
|
business code. Used for ops whose success depends on a
|
||||||
|
real reachable DB we can't guarantee in self-test.
|
||||||
|
"""
|
||||||
|
ok, detail = summarize(data)
|
||||||
|
if mode == "roundtrip":
|
||||||
|
exception = isinstance(data, dict) and "_exception" in data
|
||||||
|
passed = not exception
|
||||||
|
else:
|
||||||
|
passed = ok
|
||||||
|
status = "PASS" if passed else "FAIL"
|
||||||
|
RESULTS.append((name, status, detail))
|
||||||
|
print(f"[{status}] {name}: {detail}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def dig(data, *keys, default=None):
|
||||||
|
"""Safely walk nested dict keys."""
|
||||||
|
cur = data
|
||||||
|
for k in keys:
|
||||||
|
if not isinstance(cur, dict):
|
||||||
|
return default
|
||||||
|
cur = cur.get(k)
|
||||||
|
return cur if cur is not None else default
|
||||||
|
|
||||||
|
|
||||||
|
async def main():
|
||||||
|
PREFIX = f"selftest_{uuid.uuid4().hex[:8]}_"
|
||||||
|
state = {}
|
||||||
|
|
||||||
|
# ---- 1. read-only: connections & configs ----------------------------
|
||||||
|
print("\n=== 只读查询 ===")
|
||||||
|
conns = record("list_connections", await call("list_connections", {"pageSize": 5}))
|
||||||
|
# pick a builtin connection to exercise builtin/realtime tools
|
||||||
|
rows = conns.get("rows") or dig(conns, "data", "rows", default=[]) or []
|
||||||
|
builtin_id = None
|
||||||
|
any_conn_id = None
|
||||||
|
for r in rows:
|
||||||
|
cid = r.get("id")
|
||||||
|
if any_conn_id is None:
|
||||||
|
any_conn_id = cid
|
||||||
|
if r.get("sourceType") == "builtin" and builtin_id is None:
|
||||||
|
builtin_id = cid
|
||||||
|
state["builtin_id"] = builtin_id
|
||||||
|
state["any_conn_id"] = any_conn_id
|
||||||
|
print(f" -> builtin_id={builtin_id}, any_conn_id={any_conn_id}")
|
||||||
|
|
||||||
|
record("list_datasource_configs", await call("list_datasource_configs", {"pageSize": 5}))
|
||||||
|
|
||||||
|
if any_conn_id:
|
||||||
|
record("get_connection", await call("get_connection", {"id": any_conn_id}))
|
||||||
|
record("realtime_databases", await call("realtime_databases", {"id": any_conn_id}))
|
||||||
|
record("realtime_structure", await call("realtime_structure", {"id": any_conn_id}))
|
||||||
|
else:
|
||||||
|
for n in ("get_connection", "realtime_databases", "realtime_structure"):
|
||||||
|
RESULTS.append((n, "SKIP", "no connection available"))
|
||||||
|
|
||||||
|
# ---- 2. create a builtin PostgreSQL connection ----------------------
|
||||||
|
print("\n=== 创建内置连接 ===")
|
||||||
|
created = record(
|
||||||
|
"create_builtin_postgresql",
|
||||||
|
await call("create_builtin_postgresql", {
|
||||||
|
"datasourceName": PREFIX + "conn",
|
||||||
|
"remark": "self-test connection",
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
conn_id = dig(created, "data", "id")
|
||||||
|
if conn_id is None and builtin_id:
|
||||||
|
conn_id = builtin_id # fall back to an existing builtin for downstream tools
|
||||||
|
state["conn_id"] = conn_id
|
||||||
|
print(f" -> conn_id={conn_id}")
|
||||||
|
|
||||||
|
if conn_id:
|
||||||
|
record("update_builtin_database", await call("update_builtin_database", {
|
||||||
|
"connectionId": conn_id,
|
||||||
|
"datasourceName": PREFIX + "conn_renamed",
|
||||||
|
"remark": "renamed by self-test",
|
||||||
|
}))
|
||||||
|
record("get_connection", await call("get_connection", {"id": conn_id}))
|
||||||
|
|
||||||
|
# ---- 3. external connection: test/create/update/status/delete -------
|
||||||
|
print("\n=== 外部连接测试(依赖真实可达的库,可能失败属正常)===")
|
||||||
|
# Try to spin up a local Docker Postgres mock. If Docker is unavailable or
|
||||||
|
# the backend cannot reach the host IP, fall back to 127.0.0.1 and keep the
|
||||||
|
# round-trip assertion (we still verify the tool round-trips).
|
||||||
|
mock_host = "127.0.0.1"
|
||||||
|
try:
|
||||||
|
mock_host = start_mock_db()
|
||||||
|
print(f" -> mock DB available at {mock_host}:5432")
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" -> could not start mock DB, using 127.0.0.1:5432 ({e})")
|
||||||
|
|
||||||
|
# test_connection_config / test_connection rely on datasourceType default now
|
||||||
|
record(
|
||||||
|
"test_connection_config",
|
||||||
|
await call("test_connection_config", {
|
||||||
|
"host": mock_host, "port": 5432,
|
||||||
|
"username": "postgres", "password": "postgres",
|
||||||
|
}),
|
||||||
|
mode="roundtrip", # may still fail if backend cannot reach mock_host
|
||||||
|
)
|
||||||
|
|
||||||
|
# create_connection (external) — exercises datasourceType/connectionType defaults
|
||||||
|
ext = record(
|
||||||
|
"create_connection",
|
||||||
|
await call("create_connection", {
|
||||||
|
"datasourceName": PREFIX + "ext",
|
||||||
|
"host": mock_host, "port": 5432,
|
||||||
|
"username": "postgres", "password": "postgres",
|
||||||
|
"remark": "self-test external",
|
||||||
|
}),
|
||||||
|
mode="roundtrip",
|
||||||
|
)
|
||||||
|
ext_id = dig(ext, "data", "id")
|
||||||
|
state["ext_id"] = ext_id
|
||||||
|
print(f" -> ext_id={ext_id}")
|
||||||
|
|
||||||
|
if ext_id:
|
||||||
|
record("update_connection", await call("update_connection", {
|
||||||
|
"id": ext_id, "datasourceName": PREFIX + "ext_renamed",
|
||||||
|
"remark": "renamed",
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("test_connection", await call("test_connection", {
|
||||||
|
"id": ext_id, "host": mock_host, "port": 5432,
|
||||||
|
"username": "postgres", "password": "postgres",
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("change_connection_status", await call("change_connection_status", {
|
||||||
|
"id": ext_id, "status": 1,
|
||||||
|
}), mode="roundtrip")
|
||||||
|
else:
|
||||||
|
for n in ("update_connection", "test_connection", "change_connection_status"):
|
||||||
|
RESULTS.append((n, "SKIP", "no external connection id returned"))
|
||||||
|
|
||||||
|
# ---- 4. DDL on the builtin connection ------------------------------
|
||||||
|
print("\n=== DDL:库/表 ===")
|
||||||
|
ddl_conn = conn_id or builtin_id
|
||||||
|
db_name = PREFIX + "db"
|
||||||
|
tbl_name = PREFIX + "users"
|
||||||
|
cols = [
|
||||||
|
{"columnName": "id", "columnType": "BIGINT", "isPrimaryKey": True,
|
||||||
|
"isNullable": False, "columnComment": "主键"},
|
||||||
|
{"columnName": "name", "columnType": "VARCHAR", "columnLength": 100,
|
||||||
|
"isNullable": False, "columnComment": "姓名"},
|
||||||
|
{"columnName": "age", "columnType": "INTEGER", "isNullable": True,
|
||||||
|
"columnComment": "年龄"},
|
||||||
|
]
|
||||||
|
if ddl_conn:
|
||||||
|
record("create_database", await call("create_database", {
|
||||||
|
"connectionId": ddl_conn, "databaseName": db_name,
|
||||||
|
"encoding": "UTF8",
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("create_table", await call("create_table", {
|
||||||
|
"connectionId": ddl_conn, "databaseName": db_name,
|
||||||
|
"tableName": tbl_name, "tableComment": "self-test 用户表",
|
||||||
|
"columns": cols,
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("realtime_tables", await call("realtime_tables", {
|
||||||
|
"id": ddl_conn, "databaseName": db_name,
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("create_database_table", await call("create_database_table", {
|
||||||
|
"connectionId": ddl_conn, "databaseName": PREFIX + "db2",
|
||||||
|
"encoding": "UTF8",
|
||||||
|
"tables": [{"tableName": PREFIX + "t2", "tableComment": "二号表",
|
||||||
|
"columns": cols}],
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("alter_table", await call("alter_table", {
|
||||||
|
"connectionId": ddl_conn, "databaseName": db_name,
|
||||||
|
"tableName": tbl_name,
|
||||||
|
"operations": [{"operation": "ADD_COLUMN", "column": {
|
||||||
|
"columnName": "email", "columnType": "VARCHAR",
|
||||||
|
"columnLength": 255, "isNullable": True, "columnComment": "邮箱"}}],
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("alter_database", await call("alter_database", {
|
||||||
|
"connectionId": ddl_conn, "databaseName": PREFIX + "db2",
|
||||||
|
"newName": PREFIX + "db2_renamed",
|
||||||
|
}), mode="roundtrip")
|
||||||
|
else:
|
||||||
|
for n in ("create_database", "create_table", "create_database_table",
|
||||||
|
"alter_table", "alter_database"):
|
||||||
|
RESULTS.append((n, "SKIP", "no builtin connection for DDL"))
|
||||||
|
|
||||||
|
# ---- 5. find the table id, exercise execute_sql + builtin CRUD ------
|
||||||
|
print("\n=== SQL 执行 + 内置表数据 CRUD ===")
|
||||||
|
# locate a datasource config id + table id for our table
|
||||||
|
cfgs = await call("list_datasource_configs", {"datasourceName": PREFIX, "pageSize": 20})
|
||||||
|
cfg_rows = cfgs.get("rows") or dig(cfgs, "data", "rows", default=[]) or []
|
||||||
|
datasource_id = cfg_rows[0].get("id") if cfg_rows else None
|
||||||
|
state["datasource_id"] = datasource_id
|
||||||
|
|
||||||
|
table_id = None
|
||||||
|
if ddl_conn:
|
||||||
|
detail = await call("get_connection", {"id": ddl_conn})
|
||||||
|
for ds in dig(detail, "data", "datasourceConfig", default=[]) or []:
|
||||||
|
for t in ds.get("tables", []) or []:
|
||||||
|
if str(t.get("tableName", "")).startswith(PREFIX):
|
||||||
|
table_id = t.get("tableId") or t.get("id")
|
||||||
|
break
|
||||||
|
if table_id:
|
||||||
|
break
|
||||||
|
state["table_id"] = table_id
|
||||||
|
print(f" -> datasource_id={datasource_id}, table_id={table_id}")
|
||||||
|
|
||||||
|
if datasource_id:
|
||||||
|
record("execute_sql", await call("execute_sql", {
|
||||||
|
"datasourceId": datasource_id,
|
||||||
|
"sql": "SELECT 1",
|
||||||
|
"databaseName": db_name,
|
||||||
|
}), mode="roundtrip")
|
||||||
|
else:
|
||||||
|
RESULTS.append(("execute_sql", "SKIP", "no datasource config id"))
|
||||||
|
|
||||||
|
if table_id:
|
||||||
|
record("builtin_table_insert", await call("builtin_table_insert", {
|
||||||
|
"tableId": table_id, "data": {"id": 1, "name": "张三", "age": 25},
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("builtin_table_data", await call("builtin_table_data", {
|
||||||
|
"tableId": table_id,
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("builtin_table_update", await call("builtin_table_update", {
|
||||||
|
"tableId": table_id, "data": {"name": "李四", "age": 30},
|
||||||
|
"primaryKey": {"id": 1},
|
||||||
|
}), mode="roundtrip")
|
||||||
|
record("builtin_table_delete", await call("builtin_table_delete", {
|
||||||
|
"tableId": table_id, "primaryKeys": [{"id": 1}],
|
||||||
|
}), mode="roundtrip")
|
||||||
|
else:
|
||||||
|
for n in ("builtin_table_insert", "builtin_table_data",
|
||||||
|
"builtin_table_update", "builtin_table_delete"):
|
||||||
|
RESULTS.append((n, "SKIP", "no table id found"))
|
||||||
|
|
||||||
|
# ---- 6. AI generate + document import (preview/confirm) -------------
|
||||||
|
print("\n=== AI 生成 + 文档导入 ===")
|
||||||
|
record("generate_table", await call("generate_table", {
|
||||||
|
"requirement": "一个简单的待办事项表,含标题、状态、创建时间",
|
||||||
|
"databaseId": datasource_id,
|
||||||
|
}), mode="roundtrip")
|
||||||
|
|
||||||
|
# build a tiny CSV for import preview
|
||||||
|
csv_path = os.path.join(tempfile.gettempdir(), "selftest_import.csv")
|
||||||
|
with open(csv_path, "w", newline="", encoding="utf-8") as f:
|
||||||
|
w = csv.writer(f)
|
||||||
|
w.writerow(["name", "age", "gender"])
|
||||||
|
w.writerow(["张三", "25", "male"])
|
||||||
|
w.writerow(["李四", "30", "female"])
|
||||||
|
|
||||||
|
preview = None
|
||||||
|
if ddl_conn:
|
||||||
|
preview = record("import_document_preview", await call("import_document_preview", {
|
||||||
|
"connectionId": ddl_conn, "filePath": csv_path,
|
||||||
|
}), mode="roundtrip")
|
||||||
|
ts = dig(preview, "data", "tableStructure")
|
||||||
|
all_data = dig(preview, "data", "allData")
|
||||||
|
if ts and all_data:
|
||||||
|
record("import_document_confirm", await call("import_document_confirm", {
|
||||||
|
"connectionId": ddl_conn, "tableStructure": ts, "allData": all_data,
|
||||||
|
}), mode="roundtrip")
|
||||||
|
else:
|
||||||
|
RESULTS.append(("import_document_confirm", "SKIP",
|
||||||
|
"preview returned no tableStructure/allData"))
|
||||||
|
else:
|
||||||
|
for n in ("import_document_preview", "import_document_confirm"):
|
||||||
|
RESULTS.append((n, "SKIP", "no builtin connection"))
|
||||||
|
|
||||||
|
# ---- 7. datasource config batch ops --------------------------------
|
||||||
|
print("\n=== 数据源配置批量操作 ===")
|
||||||
|
if ddl_conn:
|
||||||
|
record("batch_create_datasource_configs", await call(
|
||||||
|
"batch_create_datasource_configs", {
|
||||||
|
"connectionId": ddl_conn,
|
||||||
|
"datasourceNamePrefix": PREFIX + "cfg",
|
||||||
|
"syncTables": False,
|
||||||
|
"databases": [{"databaseName": db_name, "tableNames": []}],
|
||||||
|
}), mode="roundtrip")
|
||||||
|
else:
|
||||||
|
RESULTS.append(("batch_create_datasource_configs", "SKIP", "no connection"))
|
||||||
|
|
||||||
|
if datasource_id:
|
||||||
|
record("get_datasource_config", await call("get_datasource_config", {
|
||||||
|
"id": datasource_id}), mode="roundtrip")
|
||||||
|
record("change_datasource_status", await call("change_datasource_status", {
|
||||||
|
"id": datasource_id, "status": 1}), mode="roundtrip")
|
||||||
|
record("batch_update_datasource_configs", await call(
|
||||||
|
"batch_update_datasource_configs", {
|
||||||
|
"syncTables": False,
|
||||||
|
"datasources": [{"id": datasource_id, "tableNames": []}],
|
||||||
|
}), mode="roundtrip")
|
||||||
|
else:
|
||||||
|
for n in ("get_datasource_config", "change_datasource_status",
|
||||||
|
"batch_update_datasource_configs"):
|
||||||
|
RESULTS.append((n, "SKIP", "no datasource config id"))
|
||||||
|
|
||||||
|
record("export_datasource_configs", await call("export_datasource_configs", {
|
||||||
|
"datasourceName": PREFIX}), mode="roundtrip")
|
||||||
|
|
||||||
|
# replace_datasource_configs intentionally skipped (destructive)
|
||||||
|
RESULTS.append(("replace_datasource_configs", "SKIP",
|
||||||
|
"destructive full-replace, skipped by design"))
|
||||||
|
|
||||||
|
# ---- 8. cleanup -----------------------------------------------------
|
||||||
|
print("\n=== 清理 selftest_ 资源 ===")
|
||||||
|
# delete selftest datasource configs
|
||||||
|
cfgs2 = await call("list_datasource_configs", {"datasourceName": PREFIX, "pageSize": 50})
|
||||||
|
cfg_rows2 = cfgs2.get("rows") or dig(cfgs2, "data", "rows", default=[]) or []
|
||||||
|
del_ids = [r.get("id") for r in cfg_rows2 if r.get("id") is not None]
|
||||||
|
if del_ids:
|
||||||
|
record("delete_datasource_configs", await call("delete_datasource_configs", {
|
||||||
|
"ids": del_ids}), mode="roundtrip")
|
||||||
|
else:
|
||||||
|
RESULTS.append(("delete_datasource_configs", "SKIP", "nothing to delete"))
|
||||||
|
|
||||||
|
# delete selftest connections
|
||||||
|
deleted_conn = False
|
||||||
|
for cid in (state.get("ext_id"), conn_id):
|
||||||
|
if cid:
|
||||||
|
record("delete_connection", await call("delete_connection", {"id": cid}),
|
||||||
|
mode="roundtrip")
|
||||||
|
deleted_conn = True
|
||||||
|
if not deleted_conn:
|
||||||
|
RESULTS.append(("delete_connection", "SKIP", "no selftest connection to delete"))
|
||||||
|
|
||||||
|
try:
|
||||||
|
os.remove(csv_path)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
try:
|
||||||
|
stop_mock_db()
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
print(f" -> failed to stop mock DB: {e}")
|
||||||
|
|
||||||
|
# ---- summary --------------------------------------------------------
|
||||||
|
print("\n" + "=" * 60)
|
||||||
|
print("自测结果汇总")
|
||||||
|
print("=" * 60)
|
||||||
|
all_names = {t["name"] for t in ALL_TOOLS}
|
||||||
|
counts = {"PASS": 0, "FAIL": 0, "SKIP": 0}
|
||||||
|
for name, status, detail in RESULTS:
|
||||||
|
counts[status] = counts.get(status, 0) + 1
|
||||||
|
for name, status, detail in RESULTS:
|
||||||
|
print(f" [{status}] {name}: {detail}")
|
||||||
|
untested = all_names - TESTED
|
||||||
|
print("-" * 60)
|
||||||
|
print(f"工具总数: {len(all_names)} 覆盖: {len(TESTED)} 未触达: {sorted(untested)}")
|
||||||
|
print(f"PASS={counts['PASS']} FAIL={counts['FAIL']} SKIP={counts['SKIP']}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
asyncio.run(main())
|
||||||
4
lzwcai_mcp_agile_db_third/uv.toml
Normal file
4
lzwcai_mcp_agile_db_third/uv.toml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[[index]]
|
||||||
|
name = "pypi"
|
||||||
|
url = "https://pypi.org/simple/"
|
||||||
|
default = true
|
||||||
@@ -94,3 +94,11 @@
|
|||||||
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
|
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
|
||||||
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
|
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
|
||||||
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
|
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
|
||||||
|
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:215] - ================================================================================
|
||||||
|
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:216] - 日志系统初始化完成 - 2026-06-16 10:55:20
|
||||||
|
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:217] - 日志级别: INFO
|
||||||
|
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:218] - 日志文件: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_api_converter\lzwcai_mcp_api_converter.log
|
||||||
|
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:219] - 控制台输出: False
|
||||||
|
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
|
||||||
|
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
|
||||||
|
2026-06-16 10:55:20 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:222] - ================================================================================
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
lzwcai_mcp_sqlexecutor/__pycache__/main.cpython-312.pyc
Normal file
BIN
lzwcai_mcp_sqlexecutor/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1,223 +0,0 @@
|
|||||||
2025-12-31 12:57:27 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs
|
|
||||||
2025-12-31 12:57:27 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:347] - 开始运行 MCP SQL Executor 服务器
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:299] - ============================================================
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:300] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:301] - 版本: 0.1.0
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:302] - ============================================================
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:306] - 环境配置 - Database ID: 16
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:307] - 环境配置 - Skill ID:
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:308] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:309] - ============================================================
|
|
||||||
2025-12-31 12:57:27 - mcp_services - INFO - [main.py:314] - MCP 服务器已启动,等待客户端连接...
|
|
||||||
2025-12-31 12:57:28 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest
|
|
||||||
2025-12-31 12:57:28 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
|
|
||||||
2025-12-31 12:57:28 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api)...
|
|
||||||
2025-12-31 12:57:28 - mcp_services - INFO - [main.py:278] - 调用第三方API,skill_id:
|
|
||||||
2025-12-31 12:57:28 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://192.168.11.24:8088/datasource/skill/getBySkillId/
|
|
||||||
2025-12-31 12:57:29 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.11.24:8088/datasource/skill/getBySkillId/ "HTTP/1.1 404 "
|
|
||||||
2025-12-31 12:57:29 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:97] - API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/
|
|
||||||
2025-12-31 12:57:29 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:98] - 错误响应: {"timestamp":"2025-12-31T12:57:30.248+08:00","status":404,"error":"Not Found","path":"/datasource/skill/getBySkillId/"}
|
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:292] - API调用失败: API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 80, in get_skill_by_id
|
|
||||||
response.raise_for_status()
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_models.py", line 829, in raise_for_status
|
|
||||||
raise HTTPStatusError(message, request=request, response=self)
|
|
||||||
httpx.HTTPStatusError: Client error '404 ' for url 'http://192.168.11.24:8088/datasource/skill/getBySkillId/'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
|
|
||||||
|
|
||||||
During handling of the above exception, another exception occurred:
|
|
||||||
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 281, in call_third_party_api
|
|
||||||
raw_result = get_skill_by_id(skill_id)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 208, in get_skill_by_id
|
|
||||||
return default_client.get_skill_by_id(skill_id)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 99, in get_skill_by_id
|
|
||||||
raise Exception(error_msg)
|
|
||||||
Exception: API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/
|
|
||||||
2025-12-31 12:57:29 - mcp_services - WARNING - [main.py:131] - API获取失败,降级使用本地配置: API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/
|
|
||||||
2025-12-31 12:57:29 - mcp_services - INFO - [main.py:55] - 成功加载 3 个业务查询配置
|
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools
|
|
||||||
tool = generate_tool_schema_from_query(query)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:29 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest
|
|
||||||
2025-12-31 12:57:29 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
|
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools
|
|
||||||
tool = generate_tool_schema_from_query(query)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:54 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs
|
|
||||||
2025-12-31 12:57:54 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:347] - 开始运行 MCP SQL Executor 服务器
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:299] - ============================================================
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:300] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:301] - 版本: 0.1.0
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:302] - ============================================================
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:306] - 环境配置 - Database ID: 16
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:307] - 环境配置 - Skill ID:
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:308] - 环境配置 - Backend Base URL: http://192.168.11.24:8088
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:309] - ============================================================
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:314] - MCP 服务器已启动,等待客户端连接...
|
|
||||||
2025-12-31 12:57:54 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: local)...
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:55] - 成功加载 3 个业务查询配置
|
|
||||||
2025-12-31 12:57:54 - mcp_services - INFO - [main.py:123] - 本地配置: 3 条
|
|
||||||
2025-12-31 12:57:54 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:54 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools
|
|
||||||
tool = generate_tool_schema_from_query(query)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 15:00:31 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest
|
|
||||||
2025-12-31 15:00:31 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
|
|
||||||
2025-12-31 15:00:31 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 15:00:31 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools
|
|
||||||
tool = generate_tool_schema_from_query(query)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 15:00:34 - mcp_services - INFO - [main.py:329] - MCP 服务器已关闭
|
|
||||||
2025-12-31 15:00:53 - root - INFO - [logger_config.py:151] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\logs
|
|
||||||
2025-12-31 15:00:53 - root - INFO - [logger_config.py:152] - 日志配置 - 级别: INFO, 文件大小限制: 10MB, 备份数量: 5
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:347] - 开始运行 MCP SQL Executor 服务器
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:299] - ============================================================
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:300] - 正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:301] - 版本: 0.1.0
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:302] - ============================================================
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:306] - 环境配置 - Database ID: 29
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:307] - 环境配置 - Skill ID:
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:308] - 环境配置 - Backend Base URL: http://lzwcai-demp-corp-manager:8086
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:309] - ============================================================
|
|
||||||
2025-12-31 15:00:53 - mcp_services - INFO - [main.py:314] - MCP 服务器已启动,等待客户端连接...
|
|
||||||
2025-12-31 15:00:54 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest
|
|
||||||
2025-12-31 15:00:54 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
|
|
||||||
2025-12-31 15:00:54 - mcp_services - INFO - [main.py:119] - 初始化查询配置(数据源: api)...
|
|
||||||
2025-12-31 15:00:54 - mcp_services - INFO - [main.py:278] - 调用第三方API,skill_id:
|
|
||||||
2025-12-31 15:00:54 - lzwcai_mcp_sqlexecutor.utils.api_client - INFO - [api_client.py:71] - 正在调用API: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/
|
|
||||||
2025-12-31 15:00:56 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:103] - API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
|
||||||
2025-12-31 15:00:56 - mcp_services - ERROR - [main.py:292] - API调用失败: API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 101, in map_httpcore_exceptions
|
|
||||||
yield
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 250, in handle_request
|
|
||||||
resp = self._pool.handle_request(req)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 216, in handle_request
|
|
||||||
raise exc from None
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 196, in handle_request
|
|
||||||
response = connection.handle_request(
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 99, in handle_request
|
|
||||||
raise exc
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 76, in handle_request
|
|
||||||
stream = self._connect(request)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 122, in _connect
|
|
||||||
stream = self._network_backend.connect_tcp(**kwargs)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_backends\sync.py", line 205, in connect_tcp
|
|
||||||
with map_exceptions(exc_map):
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__
|
|
||||||
self.gen.throw(value)
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_exceptions.py", line 14, in map_exceptions
|
|
||||||
raise to_exc(exc) from exc
|
|
||||||
httpcore.ConnectError: [Errno 11001] getaddrinfo failed
|
|
||||||
|
|
||||||
The above exception was the direct cause of the following exception:
|
|
||||||
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 74, in get_skill_by_id
|
|
||||||
response = self.client.get(
|
|
||||||
^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 1053, in get
|
|
||||||
return self.request(
|
|
||||||
^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 825, in request
|
|
||||||
return self.send(request, auth=auth, follow_redirects=follow_redirects)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 914, in send
|
|
||||||
response = self._send_handling_auth(
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 942, in _send_handling_auth
|
|
||||||
response = self._send_handling_redirects(
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 979, in _send_handling_redirects
|
|
||||||
response = self._send_single_request(request)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 1014, in _send_single_request
|
|
||||||
response = transport.handle_request(request)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 249, in handle_request
|
|
||||||
with map_httpcore_exceptions():
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__
|
|
||||||
self.gen.throw(value)
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 118, in map_httpcore_exceptions
|
|
||||||
raise mapped_exc(message) from exc
|
|
||||||
httpx.ConnectError: [Errno 11001] getaddrinfo failed
|
|
||||||
|
|
||||||
During handling of the above exception, another exception occurred:
|
|
||||||
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 281, in call_third_party_api
|
|
||||||
raw_result = get_skill_by_id(skill_id)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 208, in get_skill_by_id
|
|
||||||
return default_client.get_skill_by_id(skill_id)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 104, in get_skill_by_id
|
|
||||||
raise Exception(error_msg)
|
|
||||||
Exception: API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
|
||||||
2025-12-31 15:00:56 - mcp_services - WARNING - [main.py:131] - API获取失败,降级使用本地配置: API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
|
||||||
2025-12-31 15:00:56 - mcp_services - INFO - [main.py:55] - 成功加载 0 个业务查询配置
|
|
||||||
2025-12-31 15:00:56 - mcp_services - INFO - [main.py:165] - 成功生成 0 个 MCP 工具
|
|
||||||
2025-12-31 15:00:56 - mcp.server.lowlevel.server - INFO - [server.py:619] - Processing request of type ListToolsRequest
|
|
||||||
2025-12-31 15:00:56 - mcp_services - INFO - [main.py:156] - 收到列出工具请求
|
|
||||||
2025-12-31 15:00:56 - mcp_services - INFO - [main.py:165] - 成功生成 0 个 MCP 工具
|
|
||||||
@@ -1,110 +1,26 @@
|
|||||||
2025-12-31 12:57:29 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:97] - API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/
|
2026-05-25 16:22:53 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:69] - API 请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
||||||
2025-12-31 12:57:29 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:98] - 错误响应: {"timestamp":"2025-12-31T12:57:30.248+08:00","status":404,"error":"Not Found","path":"/datasource/skill/getBySkillId/"}
|
2026-05-25 16:22:53 - mcp_services - ERROR - [main.py:209] - API 调用失败: API 请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:292] - API调用失败: API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 80, in get_skill_by_id
|
|
||||||
response.raise_for_status()
|
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_models.py", line 829, in raise_for_status
|
|
||||||
raise HTTPStatusError(message, request=request, response=self)
|
|
||||||
httpx.HTTPStatusError: Client error '404 ' for url 'http://192.168.11.24:8088/datasource/skill/getBySkillId/'
|
|
||||||
For more information check: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
|
|
||||||
|
|
||||||
During handling of the above exception, another exception occurred:
|
|
||||||
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 281, in call_third_party_api
|
|
||||||
raw_result = get_skill_by_id(skill_id)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 208, in get_skill_by_id
|
|
||||||
return default_client.get_skill_by_id(skill_id)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 99, in get_skill_by_id
|
|
||||||
raise Exception(error_msg)
|
|
||||||
Exception: API请求失败 (HTTP 404): http://192.168.11.24:8088/datasource/skill/getBySkillId/
|
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools
|
|
||||||
tool = generate_tool_schema_from_query(query)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:29 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools
|
|
||||||
tool = generate_tool_schema_from_query(query)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:54 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 12:57:54 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools
|
|
||||||
tool = generate_tool_schema_from_query(query)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 15:00:31 - mcp_services - ERROR - [main.py:92] - 生成工具模式失败: 2006300000000000001, 错误: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 15:00:31 - mcp_services - ERROR - [main.py:170] - 列出工具失败: 'businessName'
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 162, in handle_list_tools
|
|
||||||
tool = generate_tool_schema_from_query(query)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 80, in generate_tool_schema_from_query
|
|
||||||
tool_name = query['businessName']
|
|
||||||
~~~~~^^^^^^^^^^^^^^^^
|
|
||||||
KeyError: 'businessName'
|
|
||||||
2025-12-31 15:00:56 - lzwcai_mcp_sqlexecutor.utils.api_client - ERROR - [api_client.py:103] - API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
|
||||||
2025-12-31 15:00:56 - mcp_services - ERROR - [main.py:292] - API调用失败: API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 101, in map_httpcore_exceptions
|
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 101, in map_httpcore_exceptions
|
||||||
yield
|
yield
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 250, in handle_request
|
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 250, in handle_request
|
||||||
resp = self._pool.handle_request(req)
|
resp = self._pool.handle_request(req)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 216, in handle_request
|
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 256, in handle_request
|
||||||
raise exc from None
|
raise exc from None
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 196, in handle_request
|
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection_pool.py", line 236, in handle_request
|
||||||
response = connection.handle_request(
|
response = connection.handle_request(
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 99, in handle_request
|
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 101, in handle_request
|
||||||
raise exc
|
raise exc
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 76, in handle_request
|
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 78, in handle_request
|
||||||
stream = self._connect(request)
|
stream = self._connect(request)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 122, in _connect
|
File "D:\anaconda3\Lib\site-packages\httpcore\_sync\connection.py", line 124, in _connect
|
||||||
stream = self._network_backend.connect_tcp(**kwargs)
|
stream = self._network_backend.connect_tcp(**kwargs)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_backends\sync.py", line 205, in connect_tcp
|
File "D:\anaconda3\Lib\site-packages\httpcore\_backends\sync.py", line 207, in connect_tcp
|
||||||
with map_exceptions(exc_map):
|
with map_exceptions(exc_map):
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__
|
File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__
|
||||||
self.gen.throw(value)
|
self.gen.throw(value)
|
||||||
File "D:\anaconda3\Lib\site-packages\httpcore\_exceptions.py", line 14, in map_exceptions
|
File "D:\anaconda3\Lib\site-packages\httpcore\_exceptions.py", line 14, in map_exceptions
|
||||||
@@ -114,9 +30,9 @@ httpcore.ConnectError: [Errno 11001] getaddrinfo failed
|
|||||||
The above exception was the direct cause of the following exception:
|
The above exception was the direct cause of the following exception:
|
||||||
|
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 74, in get_skill_by_id
|
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 50, in get_skill_by_id
|
||||||
response = self.client.get(
|
response = self.client.get(url, headers=self._get_headers())
|
||||||
^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 1053, in get
|
File "D:\anaconda3\Lib\site-packages\httpx\_client.py", line 1053, in get
|
||||||
return self.request(
|
return self.request(
|
||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
@@ -137,7 +53,6 @@ Traceback (most recent call last):
|
|||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 249, in handle_request
|
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 249, in handle_request
|
||||||
with map_httpcore_exceptions():
|
with map_httpcore_exceptions():
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__
|
File "D:\anaconda3\Lib\contextlib.py", line 158, in __exit__
|
||||||
self.gen.throw(value)
|
self.gen.throw(value)
|
||||||
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 118, in map_httpcore_exceptions
|
File "D:\anaconda3\Lib\site-packages\httpx\_transports\default.py", line 118, in map_httpcore_exceptions
|
||||||
@@ -147,12 +62,12 @@ httpx.ConnectError: [Errno 11001] getaddrinfo failed
|
|||||||
During handling of the above exception, another exception occurred:
|
During handling of the above exception, another exception occurred:
|
||||||
|
|
||||||
Traceback (most recent call last):
|
Traceback (most recent call last):
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 281, in call_third_party_api
|
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\main.py", line 202, in call_third_party_api
|
||||||
raw_result = get_skill_by_id(skill_id)
|
raw_result = get_skill_by_id(skill_id)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 208, in get_skill_by_id
|
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 154, in get_skill_by_id
|
||||||
return default_client.get_skill_by_id(skill_id)
|
return default_client.get_skill_by_id(skill_id)
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 104, in get_skill_by_id
|
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_sqlexecutor\lzwcai_mcp_sqlexecutor\utils\api_client.py", line 70, in get_skill_by_id
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
Exception: API请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
Exception: API 请求异常: http://lzwcai-demp-corp-manager:8086/datasource/skill/getBySkillId/, 错误: [Errno 11001] getaddrinfo failed
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,59 +1,61 @@
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
# 支持直接运行和模块导入两种方式
|
|
||||||
try:
|
try:
|
||||||
from .utils import load_json, generate_tool_name, generate_input_schema
|
from .utils import load_json, generate_input_schema
|
||||||
from .utils import get_skill_by_id, DataSourceAPIClient, process_skill_response, test_sql_with_schema
|
from .utils import get_skill_by_id, process_skill_response, test_sql_with_schema
|
||||||
from .utils import get_database_id, get_skill_id, get_env_config
|
from .utils import get_skill_id, get_env_config
|
||||||
from .utils.logger_config import logger_config
|
from .utils.logger_config import logger_config
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from utils import load_json, generate_tool_name, generate_input_schema
|
from utils import load_json, generate_input_schema
|
||||||
from utils import get_skill_by_id, DataSourceAPIClient, process_skill_response, test_sql_with_schema
|
from utils import get_skill_by_id, process_skill_response, test_sql_with_schema
|
||||||
from utils import get_database_id, get_skill_id, get_env_config
|
from utils import get_skill_id, get_env_config
|
||||||
from utils.logger_config import logger_config
|
from utils.logger_config import logger_config
|
||||||
|
|
||||||
from mcp.server.models import InitializationOptions
|
from mcp.server.models import InitializationOptions
|
||||||
from mcp.server import NotificationOptions, Server
|
from mcp.server import NotificationOptions, Server
|
||||||
import mcp.types as types
|
import mcp.types as types
|
||||||
|
|
||||||
# 初始化 MCP 专用日志器
|
|
||||||
mcp_logger = logger_config.setup_mcp_logging()
|
mcp_logger = logger_config.setup_mcp_logging()
|
||||||
|
|
||||||
# ========== 数据源配置 ==========
|
DATA_SOURCE_API = "api"
|
||||||
# 数据源类型常量
|
DATA_SOURCE_LOCAL = "local"
|
||||||
DATA_SOURCE_API = "api" # 仅使用API数据
|
DATA_SOURCE_BOTH = "both"
|
||||||
DATA_SOURCE_LOCAL = "local" # 仅使用本地JSON数据
|
|
||||||
DATA_SOURCE_BOTH = "both" # 合并本地和API数据
|
|
||||||
|
|
||||||
# 默认数据源(可修改)
|
|
||||||
DEFAULT_DATA_SOURCE = DATA_SOURCE_API
|
DEFAULT_DATA_SOURCE = DATA_SOURCE_API
|
||||||
# ================================
|
|
||||||
|
|
||||||
|
def _text_response(payload: dict[str, Any]) -> list[types.TextContent]:
|
||||||
|
"""Build a JSON text response."""
|
||||||
|
return [
|
||||||
|
types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(payload, ensure_ascii=False, indent=2)
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_user_id(arguments: dict[str, Any]) -> Any:
|
||||||
|
"""Allow numeric 0, reject None and blank strings."""
|
||||||
|
user_id = arguments.get("userId")
|
||||||
|
if user_id is None:
|
||||||
|
return None
|
||||||
|
if isinstance(user_id, str) and not user_id.strip():
|
||||||
|
return None
|
||||||
|
return user_id
|
||||||
|
|
||||||
|
|
||||||
def get_queries():
|
def get_queries():
|
||||||
"""
|
"""Read local business query config."""
|
||||||
获取业务查询配置
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list: 包含所有业务查询配置的列表
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# 获取当前文件所在目录
|
|
||||||
current_dir = Path(__file__).parent
|
current_dir = Path(__file__).parent
|
||||||
|
|
||||||
# 构建 businessQueries.json 的路径
|
|
||||||
json_path = current_dir / "businessQueries.json"
|
json_path = current_dir / "businessQueries.json"
|
||||||
|
|
||||||
mcp_logger.debug(f"正在读取业务查询配置文件: {json_path}")
|
mcp_logger.debug(f"正在读取业务查询配置文件: {json_path}")
|
||||||
|
|
||||||
# 使用 load_json 方法读取 JSON 文件
|
|
||||||
queries = load_json(json_path)
|
queries = load_json(json_path)
|
||||||
|
|
||||||
mcp_logger.info(f"成功加载 {len(queries)} 个业务查询配置")
|
mcp_logger.info(f"成功加载 {len(queries)} 个业务查询配置")
|
||||||
|
|
||||||
return queries
|
return queries
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mcp_logger.error(f"加载业务查询配置失败: {e}", exc_info=True)
|
mcp_logger.error(f"加载业务查询配置失败: {e}", exc_info=True)
|
||||||
@@ -61,24 +63,11 @@ def get_queries():
|
|||||||
|
|
||||||
|
|
||||||
def generate_tool_schema_from_query(query: dict) -> types.Tool:
|
def generate_tool_schema_from_query(query: dict) -> types.Tool:
|
||||||
"""
|
"""Generate an MCP tool definition from one query config."""
|
||||||
根据查询配置生成 MCP 工具模式
|
|
||||||
|
|
||||||
Args:
|
|
||||||
query: 单个查询配置字典
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
types.Tool: MCP 工具对象
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# 获取参数定义并生成 inputSchema
|
parameters = query.get("parameters", {})
|
||||||
parameters = query.get('parameters', {})
|
|
||||||
input_schema = generate_input_schema(parameters)
|
input_schema = generate_input_schema(parameters)
|
||||||
|
tool_name = query["businessName"]
|
||||||
# 生成工具名称(格式: tool_拼音_id)
|
|
||||||
# tool_name = generate_tool_name(query['businessName'], query['id'])
|
|
||||||
tool_name = query['businessName']
|
|
||||||
# 构建工具描述,包含业务名称和业务描述
|
|
||||||
description = f"{query['businessName']}: {query['businessDescription']}"
|
description = f"{query['businessName']}: {query['businessDescription']}"
|
||||||
|
|
||||||
mcp_logger.debug(f"生成工具模式: {tool_name} - {query['businessName']}")
|
mcp_logger.debug(f"生成工具模式: {tool_name} - {query['businessName']}")
|
||||||
@@ -89,82 +78,57 @@ def generate_tool_schema_from_query(query: dict) -> types.Tool:
|
|||||||
inputSchema=input_schema
|
inputSchema=input_schema
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mcp_logger.error(f"生成工具模式失败: {query.get('id', 'unknown')}, 错误: {e}", exc_info=True)
|
mcp_logger.error(
|
||||||
|
f"生成工具模式失败: {query.get('id', 'unknown')}, 错误: {e}",
|
||||||
|
exc_info=True
|
||||||
|
)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
# 创建 MCP 服务器实例
|
|
||||||
server = Server("lzwcai-mcp-sqlexecutor")
|
server = Server("lzwcai-mcp-sqlexecutor")
|
||||||
|
|
||||||
# 缓存查询配置,避免重复加载
|
|
||||||
_queries_cache = None
|
_queries_cache = None
|
||||||
|
|
||||||
|
|
||||||
async def get_queries_cache(source: str = None):
|
async def get_queries_cache(source: str = None):
|
||||||
"""
|
"""Get or initialize query config cache."""
|
||||||
获取或初始化查询配置缓存
|
|
||||||
|
|
||||||
Args:
|
|
||||||
source: 数据源类型(默认使用 DEFAULT_DATA_SOURCE)
|
|
||||||
- "api": 仅使用API数据
|
|
||||||
- "local": 仅使用本地JSON数据
|
|
||||||
- "both": 合并本地和API数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
查询配置列表
|
|
||||||
"""
|
|
||||||
global _queries_cache
|
global _queries_cache
|
||||||
if _queries_cache is None:
|
if _queries_cache is None:
|
||||||
source = source or DEFAULT_DATA_SOURCE
|
source = source or DEFAULT_DATA_SOURCE
|
||||||
mcp_logger.info(f"初始化查询配置(数据源: {source})...")
|
mcp_logger.info(f"初始化查询配置,数据源: {source}")
|
||||||
|
|
||||||
if source == DATA_SOURCE_LOCAL:
|
if source == DATA_SOURCE_LOCAL:
|
||||||
_queries_cache = get_queries()
|
_queries_cache = get_queries()
|
||||||
mcp_logger.info(f"本地配置: {len(_queries_cache)} 条")
|
mcp_logger.info(f"本地配置: {len(_queries_cache)} 条")
|
||||||
|
|
||||||
elif source == DATA_SOURCE_API:
|
elif source == DATA_SOURCE_API:
|
||||||
try:
|
try:
|
||||||
_queries_cache = await call_third_party_api()
|
_queries_cache = await call_third_party_api()
|
||||||
mcp_logger.info(f"API配置: {len(_queries_cache)} 条")
|
mcp_logger.info(f"API 配置: {len(_queries_cache)} 条")
|
||||||
mcp_logger.info(f"API配置数组: {_queries_cache}")
|
mcp_logger.info(f"API 配置数组: {_queries_cache}")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mcp_logger.warning(f"API获取失败,降级使用本地配置: {e}")
|
mcp_logger.warning(f"API 获取失败,降级使用本地配置: {e}")
|
||||||
_queries_cache = get_queries()
|
_queries_cache = get_queries()
|
||||||
|
else:
|
||||||
else: # DATA_SOURCE_BOTH
|
|
||||||
local = get_queries()
|
local = get_queries()
|
||||||
try:
|
try:
|
||||||
api = await call_third_party_api()
|
api = await call_third_party_api()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mcp_logger.warning(f"API获取失败: {e}")
|
mcp_logger.warning(f"API 获取失败: {e}")
|
||||||
api = []
|
api = []
|
||||||
_queries_cache = local + api
|
_queries_cache = local + api
|
||||||
mcp_logger.info(f"配置总数: {len(_queries_cache)} 条(本地{len(local)}+API{len(api)})")
|
mcp_logger.info(f"配置总数: {len(_queries_cache)} 条,本地 {len(local)} + API {len(api)}")
|
||||||
|
|
||||||
return _queries_cache
|
return _queries_cache
|
||||||
|
|
||||||
|
|
||||||
@server.list_tools()
|
@server.list_tools()
|
||||||
async def handle_list_tools() -> list[types.Tool]:
|
async def handle_list_tools() -> list[types.Tool]:
|
||||||
"""
|
"""List all dynamically generated MCP tools."""
|
||||||
列出所有动态生成的 MCP 工具
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[types.Tool]: 所有可用的工具列表
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
mcp_logger.info("收到列出工具请求")
|
mcp_logger.info("收到列出工具请求")
|
||||||
|
|
||||||
queries = await get_queries_cache()
|
queries = await get_queries_cache()
|
||||||
tools = []
|
tools = [generate_tool_schema_from_query(query) for query in queries]
|
||||||
|
|
||||||
for query in queries:
|
|
||||||
tool = generate_tool_schema_from_query(query)
|
|
||||||
tools.append(tool)
|
|
||||||
|
|
||||||
mcp_logger.info(f"成功生成 {len(tools)} 个 MCP 工具")
|
mcp_logger.info(f"成功生成 {len(tools)} 个 MCP 工具")
|
||||||
mcp_logger.debug(f"工具列表: {[tool.name for tool in tools]}")
|
mcp_logger.debug(f"工具列表: {[tool.name for tool in tools]}")
|
||||||
|
|
||||||
return tools
|
return tools
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mcp_logger.error(f"列出工具失败: {e}", exc_info=True)
|
mcp_logger.error(f"列出工具失败: {e}", exc_info=True)
|
||||||
@@ -176,132 +140,84 @@ async def handle_call_tool(
|
|||||||
name: str,
|
name: str,
|
||||||
arguments: dict[str, Any] | None
|
arguments: dict[str, Any] | None
|
||||||
) -> list[types.TextContent]:
|
) -> list[types.TextContent]:
|
||||||
"""
|
"""Handle MCP tool invocation."""
|
||||||
处理工具调用请求
|
|
||||||
|
|
||||||
Args:
|
|
||||||
name: 工具名称
|
|
||||||
arguments: 工具参数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
list[types.TextContent]: 工具执行结果(返回参数和对应的接口配置)
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
mcp_logger.info(f"收到工具调用请求: {name}")
|
mcp_logger.info(f"收到工具调用请求: {name}")
|
||||||
mcp_logger.debug(f"工具参数: {arguments}")
|
mcp_logger.debug(f"工具参数: {arguments}")
|
||||||
|
|
||||||
# 获取查询配置缓存
|
|
||||||
queries = await get_queries_cache()
|
queries = await get_queries_cache()
|
||||||
|
tool_item = next((query for query in queries if query["businessName"] == name), None)
|
||||||
|
|
||||||
# 根据工具名称查找对应的 item(接口配置)
|
if not tool_item:
|
||||||
tool_item = None
|
return _text_response({"error": f"未找到工具 {name} 对应的配置"})
|
||||||
for query in queries:
|
|
||||||
# tool_name = generate_tool_name(query['businessName'], query['id'])
|
|
||||||
tool_name = query['businessName']
|
|
||||||
if tool_name == name:
|
|
||||||
tool_item = query
|
|
||||||
break
|
|
||||||
|
|
||||||
# 构建返回结果
|
normalized_parameters = generate_input_schema(tool_item.get("parameters"))
|
||||||
import json
|
normalized_arguments = arguments or {}
|
||||||
|
user_id = _extract_user_id(normalized_arguments)
|
||||||
|
|
||||||
|
if user_id is None:
|
||||||
|
error_msg = f"工具 {name} 缺少必填参数 userId"
|
||||||
|
mcp_logger.warning(error_msg)
|
||||||
|
return _text_response({"error": error_msg, "required": ["userId"]})
|
||||||
|
|
||||||
if tool_item:
|
|
||||||
request_data = {
|
request_data = {
|
||||||
"datasourceId": tool_item.get("datasourceId"),
|
"datasourceId": tool_item.get("datasourceId"),
|
||||||
"businessName": tool_item.get("businessName"),
|
"businessName": tool_item.get("businessName"),
|
||||||
"businessDescription": tool_item.get("businessDescription"),
|
"businessDescription": tool_item.get("businessDescription"),
|
||||||
"sqlTemplate": tool_item.get("sqlTemplate"),
|
"sqlTemplate": tool_item.get("sqlTemplate"),
|
||||||
"parameters": tool_item.get("parameters"),
|
"parameters": normalized_parameters,
|
||||||
"testParams": arguments or {}
|
"testParams": normalized_arguments,
|
||||||
|
"userId": user_id
|
||||||
}
|
}
|
||||||
|
|
||||||
# 如果 arguments 中有 targetDatabaseName 且有值,添加到 request_data
|
target_database_name = normalized_arguments.get("targetDatabaseName")
|
||||||
if arguments and arguments.get("targetDatabaseName"):
|
if target_database_name:
|
||||||
request_data["targetDatabaseName"] = arguments["targetDatabaseName"]
|
request_data["targetDatabaseName"] = target_database_name
|
||||||
mcp_logger.debug(f"添加目标数据库名称: {arguments['targetDatabaseName']}")
|
mcp_logger.debug(f"添加目标数据库名称: {target_database_name}")
|
||||||
|
|
||||||
# 调用测试SQL API
|
|
||||||
try:
|
try:
|
||||||
mcp_logger.info("正在调用测试SQL API...")
|
mcp_logger.info("正在调用测试 SQL API...")
|
||||||
api_response = test_sql_with_schema(request_data)
|
result_payload = test_sql_with_schema(request_data)
|
||||||
mcp_logger.info("测试SQL API调用成功")
|
mcp_logger.info("测试 SQL API 调用成功")
|
||||||
|
|
||||||
# 只返回 API 响应结果
|
|
||||||
result_text = json.dumps(api_response, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"调用测试SQL API失败: {str(e)}"
|
error_msg = f"调用测试 SQL API 失败: {str(e)}"
|
||||||
mcp_logger.error(error_msg, exc_info=True)
|
mcp_logger.error(error_msg, exc_info=True)
|
||||||
result_text = json.dumps({"error": error_msg}, ensure_ascii=False, indent=2)
|
result_payload = {"error": error_msg}
|
||||||
else:
|
|
||||||
error_msg = f"未找到工具 {name} 对应的配置"
|
|
||||||
result_text = json.dumps({"error": error_msg}, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
mcp_logger.debug(f"工具调用结果: {result_text}")
|
mcp_logger.debug(f"工具调用结果: {result_payload}")
|
||||||
|
return _text_response(result_payload)
|
||||||
return [
|
|
||||||
types.TextContent(
|
|
||||||
type="text",
|
|
||||||
text=result_text
|
|
||||||
)
|
|
||||||
]
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"工具调用失败: {name}, 错误: {e}"
|
error_msg = f"工具调用失败: {name}, 错误: {e}"
|
||||||
mcp_logger.error(error_msg, exc_info=True)
|
mcp_logger.error(error_msg, exc_info=True)
|
||||||
return [
|
return [types.TextContent(type="text", text=f"错误: {error_msg}")]
|
||||||
types.TextContent(
|
|
||||||
type="text",
|
|
||||||
text=f"错误: {error_msg}"
|
|
||||||
)
|
|
||||||
]
|
|
||||||
|
|
||||||
|
|
||||||
async def call_third_party_api(skill_id: str = None) -> list:
|
async def call_third_party_api(skill_id: str = None) -> list:
|
||||||
"""
|
"""Call backend API and map the response into query configs."""
|
||||||
调用第三方API获取技能信息并返回处理后的数据
|
|
||||||
|
|
||||||
Args:
|
|
||||||
skill_id: 技能ID(默认从环境变量 SKILL_ID 读取,如果未设置则使用 1981000305474482178)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
处理后的查询配置列表(businessQueries格式)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
queries = await call_third_party_api()
|
|
||||||
# 返回: [{"id": "...", "businessName": "...", ...}, ...]
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# 如果没有传入 skill_id,则从环境变量读取
|
|
||||||
if skill_id is None:
|
if skill_id is None:
|
||||||
skill_id = get_skill_id()
|
skill_id = get_skill_id()
|
||||||
|
|
||||||
mcp_logger.info(f"调用第三方API,skill_id: {skill_id}")
|
mcp_logger.info(f"调用第三方 API,skill_id: {skill_id}")
|
||||||
|
|
||||||
# 获取原始数据
|
|
||||||
raw_result = get_skill_by_id(skill_id)
|
raw_result = get_skill_by_id(skill_id)
|
||||||
|
mcp_logger.info(f"成功获取原始响应: {raw_result}")
|
||||||
|
|
||||||
mcp_logger.info(f"成功{raw_result}")
|
|
||||||
|
|
||||||
# 处理并返回
|
|
||||||
processed_queries = process_skill_response(raw_result)
|
processed_queries = process_skill_response(raw_result)
|
||||||
|
mcp_logger.info(f"成功处理 {len(processed_queries)} 条数据")
|
||||||
mcp_logger.info(f"成功获取并处理 {len(processed_queries)} 条数据")
|
|
||||||
return processed_queries
|
return processed_queries
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mcp_logger.error(f"API调用失败: {e}", exc_info=True)
|
mcp_logger.error(f"API 调用失败: {e}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
async def async_main():
|
async def async_main():
|
||||||
"""MCP 服务器异步主函数"""
|
"""Async entry for the MCP server."""
|
||||||
try:
|
try:
|
||||||
mcp_logger.info("=" * 60)
|
mcp_logger.info("=" * 60)
|
||||||
mcp_logger.info("正在启动 MCP 服务器: lzwcai-mcp-sqlexecutor")
|
mcp_logger.info("正在启动 MCP 服务: lzwcai-mcp-sqlexecutor")
|
||||||
mcp_logger.info("版本: 0.1.0")
|
mcp_logger.info("版本: 0.1.0")
|
||||||
mcp_logger.info("=" * 60)
|
mcp_logger.info("=" * 60)
|
||||||
|
|
||||||
# 输出环境配置信息
|
|
||||||
env_config = get_env_config()
|
env_config = get_env_config()
|
||||||
mcp_logger.info(f"环境配置 - Database ID: {env_config['database_id']}")
|
mcp_logger.info(f"环境配置 - Database ID: {env_config['database_id']}")
|
||||||
mcp_logger.info(f"环境配置 - Skill ID: {env_config['skill_id']}")
|
mcp_logger.info(f"环境配置 - Skill ID: {env_config['skill_id']}")
|
||||||
@@ -311,8 +227,7 @@ async def async_main():
|
|||||||
from mcp.server.stdio import stdio_server
|
from mcp.server.stdio import stdio_server
|
||||||
|
|
||||||
async with stdio_server() as (read_stream, write_stream):
|
async with stdio_server() as (read_stream, write_stream):
|
||||||
mcp_logger.info("MCP 服务器已启动,等待客户端连接...")
|
mcp_logger.info("MCP 服务已启动,等待客户端连接...")
|
||||||
|
|
||||||
await server.run(
|
await server.run(
|
||||||
read_stream,
|
read_stream,
|
||||||
write_stream,
|
write_stream,
|
||||||
@@ -326,29 +241,24 @@ async def async_main():
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
mcp_logger.info("MCP 服务器已关闭")
|
mcp_logger.info("MCP 服务已关闭")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mcp_logger.error(f"MCP 服务器运行失败: {e}", exc_info=True)
|
mcp_logger.error(f"MCP 服务运行失败: {e}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
"""入口点函数(用于 console_scripts)"""
|
"""Console entrypoint."""
|
||||||
try:
|
try:
|
||||||
# 初始化系统日志
|
|
||||||
# MCP协议使用stdio通信,必须禁用控制台输出以避免干扰JSON-RPC通信
|
|
||||||
logger_config.setup_logging(
|
logger_config.setup_logging(
|
||||||
app_name="lzwcai_mcp_sqlexecutor",
|
app_name="lzwcai_mcp_sqlexecutor",
|
||||||
log_level=logging.INFO,
|
log_level=logging.INFO,
|
||||||
console_output=False # 禁用控制台输出
|
console_output=False
|
||||||
)
|
)
|
||||||
|
mcp_logger.info("开始运行 MCP SQL Executor 服务")
|
||||||
mcp_logger.info("开始运行 MCP SQL Executor 服务器")
|
|
||||||
asyncio.run(async_main())
|
asyncio.run(async_main())
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
except KeyboardInterrupt:
|
||||||
mcp_logger.info("收到中断信号,正在关闭服务器...")
|
mcp_logger.info("收到中断信号,正在关闭服务...")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
mcp_logger.error(f"程序运行失败: {e}", exc_info=True)
|
mcp_logger.error(f"程序运行失败: {e}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|||||||
@@ -1,328 +1,209 @@
|
|||||||
"""
|
"""Backend API client helpers."""
|
||||||
第三方API调用客户端
|
|
||||||
用于调用外部数据源接口
|
import json
|
||||||
"""
|
import logging
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
import logging
|
|
||||||
import json
|
|
||||||
from typing import Dict, Any, Optional, List
|
|
||||||
|
|
||||||
# 支持直接运行和模块导入两种方式
|
|
||||||
try:
|
try:
|
||||||
from .env_config import get_backend_base_url
|
from .env_config import get_backend_base_url
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from env_config import get_backend_base_url
|
from env_config import get_backend_base_url
|
||||||
|
|
||||||
# 获取日志记录器
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class DataSourceAPIClient:
|
class DataSourceAPIClient:
|
||||||
"""数据源API客户端"""
|
"""HTTP client for backend datasource APIs."""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
base_url: Optional[str] = None,
|
base_url: Optional[str] = None,
|
||||||
token: Optional[str] = None
|
token: Optional[str] = None
|
||||||
):
|
):
|
||||||
"""
|
|
||||||
初始化API客户端
|
|
||||||
|
|
||||||
Args:
|
|
||||||
base_url: API基础URL(默认从环境变量 BACKEND_BASE_URL 读取,如果未设置则使用 http://192.168.2.236:8088)
|
|
||||||
token: 认证令牌(Bearer Token)
|
|
||||||
"""
|
|
||||||
# 如果没有传入 base_url,则从环境变量读取
|
|
||||||
if base_url is None:
|
if base_url is None:
|
||||||
base_url = get_backend_base_url()
|
base_url = get_backend_base_url()
|
||||||
|
|
||||||
self.base_url = base_url.rstrip('/')
|
self.base_url = base_url.rstrip("/")
|
||||||
self.token = token or "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjJiYTk4ODllLWM2ZGItNDQ5YS1iZmFjLTQ2YzMxODFlODg5NCJ9.dvi8zm0LsWvJ_h9zD5blnHFRxa4z4_WBm1R487ekE7HlHzrN6dnvqhK8askqT5b1EcE8myHwRzLVMoI8UOjOrw"
|
self.token = token or (
|
||||||
|
"eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6IjJiYTk4ODllLWM2ZGItNDQ5YS1i"
|
||||||
|
"ZmFjLTQ2YzMxODFlODg5NCJ9.dvi8zm0LsWvJ_h9zD5blnHFRxa4z4_WBm1R487ekE7HlHzrN6dn"
|
||||||
|
"vqhK8askqT5b1EcE8myHwRzLVMoI8UOjOrw"
|
||||||
|
)
|
||||||
self.client = httpx.Client(timeout=30.0)
|
self.client = httpx.Client(timeout=30.0)
|
||||||
|
|
||||||
def _get_headers(self) -> Dict[str, str]:
|
def _get_headers(self) -> Dict[str, str]:
|
||||||
"""
|
|
||||||
获取请求头
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
请求头字典
|
|
||||||
"""
|
|
||||||
return {
|
return {
|
||||||
'Authorization': f'Bearer {self.token}',
|
"Authorization": f"Bearer {self.token}",
|
||||||
}
|
}
|
||||||
|
|
||||||
def get_skill_by_id(self, skill_id: str) -> Dict[str, Any]:
|
def get_skill_by_id(self, skill_id: str) -> Dict[str, Any]:
|
||||||
"""
|
"""Fetch skill details by skill id."""
|
||||||
根据技能ID获取技能信息
|
|
||||||
|
|
||||||
Args:
|
|
||||||
skill_id: 技能ID
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
API响应数据
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: 请求失败时抛出
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
url = f"{self.base_url}/datasource/skill/getBySkillId/{skill_id}"
|
url = f"{self.base_url}/datasource/skill/getBySkillId/{skill_id}"
|
||||||
|
|
||||||
logger.info(f"正在调用API: {url}")
|
logger.info(f"正在调用 API: {url}")
|
||||||
logger.debug(f"请求参数 - skill_id: {skill_id}")
|
logger.debug(f"请求参数 - skill_id: {skill_id}")
|
||||||
|
|
||||||
response = self.client.get(
|
response = self.client.get(url, headers=self._get_headers())
|
||||||
url,
|
|
||||||
headers=self._get_headers()
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查HTTP状态码
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# 解析JSON响应
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
logger.info(f"API调用成功: {url}")
|
logger.info(f"API 调用成功: {url}")
|
||||||
logger.debug(f"响应数据: {data}")
|
logger.debug(f"响应数据: {data}")
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except httpx.TimeoutException:
|
except httpx.TimeoutException:
|
||||||
error_msg = f"API请求超时: {url}"
|
error_msg = f"API 请求超时: {url}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
error_msg = f"API请求失败 (HTTP {e.response.status_code}): {url}"
|
error_msg = f"API 请求失败 (HTTP {e.response.status_code}): {url}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
logger.error(f"错误响应: {e.response.text}")
|
logger.error(f"错误响应: {e.response.text}")
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
error_msg = f"API请求异常: {url}, 错误: {str(e)}"
|
error_msg = f"API 请求异常: {url}, 错误: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"处理API响应时出错: {str(e)}"
|
error_msg = f"处理 API 响应时出错: {str(e)}"
|
||||||
logger.error(error_msg, exc_info=True)
|
logger.error(error_msg, exc_info=True)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
def test_sql_with_schema(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
def test_sql_with_schema(self, request_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
"""
|
"""Call backend SQL test endpoint."""
|
||||||
测试SQL语句并返回执行结果
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request_data: 请求数据,包含以下字段:
|
|
||||||
- datasourceId: 数据源ID
|
|
||||||
- businessName: 业务名称
|
|
||||||
- businessDescription: 业务描述
|
|
||||||
- sqlTemplate: SQL模板
|
|
||||||
- parameters: 参数定义
|
|
||||||
- testParams: 测试参数
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
API响应数据
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: 请求失败时抛出
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# 详细记录传入的数据
|
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
logger.info("test_sql_with_schema 接口接收到的数据:")
|
logger.info("test_sql_with_schema 接口接收到的数据:")
|
||||||
logger.info(f"数据类型: {type(request_data)}")
|
logger.info(f"数据类型: {type(request_data)}")
|
||||||
logger.info(f"数据内容: {json.dumps(request_data, ensure_ascii=False, indent=2)}")
|
logger.info(f"数据内容: {json.dumps(request_data, ensure_ascii=False, indent=2)}")
|
||||||
logger.info(f"数据源ID: {request_data.get('datasourceId')}")
|
logger.info(f"数据源 ID: {request_data.get('datasourceId')}")
|
||||||
logger.info(f"业务名称: {request_data.get('businessName')}")
|
logger.info(f"业务名称: {request_data.get('businessName')}")
|
||||||
logger.info(f"业务描述: {request_data.get('businessDescription')}")
|
logger.info(f"业务描述: {request_data.get('businessDescription')}")
|
||||||
logger.info(f"SQL模板: {request_data.get('sqlTemplate')}")
|
logger.info(f"SQL 模板: {request_data.get('sqlTemplate')}")
|
||||||
logger.info(f"参数定义: {request_data.get('parameters')}")
|
logger.info(f"参数定义: {request_data.get('parameters')}")
|
||||||
logger.info(f"测试参数: {request_data.get('testParams')}")
|
logger.info(f"测试参数: {request_data.get('testParams')}")
|
||||||
|
if "userId" in request_data:
|
||||||
|
logger.info(f"用户 ID: {request_data.get('userId')}")
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
|
|
||||||
url = f"{self.base_url}/datasource/sqlExecutionLog/testSqlWithSchema"
|
url = f"{self.base_url}/datasource/sqlExecutionLog/testSqlWithSchema"
|
||||||
|
|
||||||
# 构建请求头(包含Content-Type)
|
|
||||||
headers = self._get_headers()
|
headers = self._get_headers()
|
||||||
headers['Content-Type'] = 'application/json'
|
headers["Content-Type"] = "application/json"
|
||||||
headers['Accept'] = '*/*'
|
headers["Accept"] = "*/*"
|
||||||
|
|
||||||
logger.info(f"正在调用测试SQL API: {url}")
|
logger.info(f"正在调用测试 SQL API: {url}")
|
||||||
logger.debug(f"请求数据: {json.dumps(request_data, ensure_ascii=False, indent=2)}")
|
logger.debug(f"请求数据: {json.dumps(request_data, ensure_ascii=False, indent=2)}")
|
||||||
|
|
||||||
# 发送POST请求
|
response = self.client.post(url, headers=headers, json=request_data)
|
||||||
response = self.client.post(
|
|
||||||
url,
|
|
||||||
headers=headers,
|
|
||||||
json=request_data
|
|
||||||
)
|
|
||||||
|
|
||||||
# 检查HTTP状态码
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
|
|
||||||
# 解析JSON响应
|
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
# 详细记录返回的数据
|
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
logger.info("test_sql_with_schema 接口返回的数据:")
|
logger.info("test_sql_with_schema 接口返回的数据:")
|
||||||
logger.info(f"HTTP状态码: {response.status_code}")
|
logger.info(f"HTTP 状态码: {response.status_code}")
|
||||||
logger.info(f"响应数据类型: {type(data)}")
|
logger.info(f"响应数据类型: {type(data)}")
|
||||||
logger.info(f"响应数据内容: {json.dumps(data, ensure_ascii=False, indent=2)}")
|
logger.info(f"响应数据内容: {json.dumps(data, ensure_ascii=False, indent=2)}")
|
||||||
if isinstance(data, dict):
|
if isinstance(data, dict):
|
||||||
logger.info(f"响应code: {data.get('code')}")
|
logger.info(f"响应 code: {data.get('code')}")
|
||||||
logger.info(f"响应msg: {data.get('msg')}")
|
logger.info(f"响应 msg: {data.get('msg')}")
|
||||||
logger.info(f"响应data: {data.get('data')}")
|
logger.info(f"响应 data: {data.get('data')}")
|
||||||
logger.info("=" * 80)
|
logger.info("=" * 80)
|
||||||
|
|
||||||
logger.info(f"测试SQL API调用成功")
|
logger.info("测试 SQL API 调用成功")
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
except httpx.TimeoutException:
|
except httpx.TimeoutException:
|
||||||
error_msg = f"测试SQL API请求超时: {url}"
|
error_msg = f"测试 SQL API 请求超时: {url}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
except httpx.HTTPStatusError as e:
|
except httpx.HTTPStatusError as e:
|
||||||
error_msg = f"测试SQL API请求失败 (HTTP {e.response.status_code}): {url}"
|
error_msg = f"测试 SQL API 请求失败 (HTTP {e.response.status_code}): {url}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
logger.error(f"错误响应: {e.response.text}")
|
logger.error(f"错误响应: {e.response.text}")
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
except httpx.RequestError as e:
|
except httpx.RequestError as e:
|
||||||
error_msg = f"测试SQL API请求异常: {url}, 错误: {str(e)}"
|
error_msg = f"测试 SQL API 请求异常: {url}, 错误: {str(e)}"
|
||||||
logger.error(error_msg)
|
logger.error(error_msg)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"处理测试SQL API响应时出错: {str(e)}"
|
error_msg = f"处理测试 SQL API 响应时出错: {str(e)}"
|
||||||
logger.error(error_msg, exc_info=True)
|
logger.error(error_msg, exc_info=True)
|
||||||
raise Exception(error_msg)
|
raise Exception(error_msg)
|
||||||
|
|
||||||
def close(self):
|
def close(self):
|
||||||
"""关闭HTTP客户端"""
|
"""Close the underlying HTTP client."""
|
||||||
self.client.close()
|
self.client.close()
|
||||||
|
|
||||||
|
|
||||||
# 创建默认客户端实例
|
|
||||||
default_client = DataSourceAPIClient()
|
default_client = DataSourceAPIClient()
|
||||||
|
|
||||||
|
|
||||||
def get_skill_by_id(skill_id: str, base_url: Optional[str] = None, token: Optional[str] = None) -> Dict[str, Any]:
|
def get_skill_by_id(
|
||||||
"""
|
skill_id: str,
|
||||||
便捷函数:根据技能ID获取技能信息
|
base_url: Optional[str] = None,
|
||||||
|
token: Optional[str] = None
|
||||||
Args:
|
) -> Dict[str, Any]:
|
||||||
skill_id: 技能ID
|
"""Convenience wrapper for skill lookup."""
|
||||||
base_url: API基础URL(可选,默认从环境变量 BACKEND_BASE_URL 读取)
|
|
||||||
token: 认证令牌(可选,使用默认值)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
API响应数据
|
|
||||||
"""
|
|
||||||
if base_url or token:
|
if base_url or token:
|
||||||
client = DataSourceAPIClient(
|
client = DataSourceAPIClient(base_url=base_url, token=token)
|
||||||
base_url=base_url,
|
|
||||||
token=token
|
|
||||||
)
|
|
||||||
return client.get_skill_by_id(skill_id)
|
return client.get_skill_by_id(skill_id)
|
||||||
else:
|
|
||||||
return default_client.get_skill_by_id(skill_id)
|
return default_client.get_skill_by_id(skill_id)
|
||||||
|
|
||||||
|
|
||||||
def test_sql_with_schema(request_data: Dict[str, Any], base_url: Optional[str] = None, token: Optional[str] = None) -> Dict[str, Any]:
|
def test_sql_with_schema(
|
||||||
"""
|
request_data: Dict[str, Any],
|
||||||
便捷函数:测试SQL语句并返回执行结果
|
base_url: Optional[str] = None,
|
||||||
|
token: Optional[str] = None
|
||||||
Args:
|
) -> Dict[str, Any]:
|
||||||
request_data: 请求数据,包含以下字段:
|
"""Convenience wrapper for SQL test endpoint."""
|
||||||
- datasourceId: 数据源ID
|
|
||||||
- businessName: 业务名称
|
|
||||||
- businessDescription: 业务描述
|
|
||||||
- sqlTemplate: SQL模板
|
|
||||||
- parameters: 参数定义
|
|
||||||
- testParams: 测试参数
|
|
||||||
base_url: API基础URL(可选,默认从环境变量 BACKEND_BASE_URL 读取)
|
|
||||||
token: 认证令牌(可选,使用默认值)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
API响应数据
|
|
||||||
"""
|
|
||||||
if base_url or token:
|
if base_url or token:
|
||||||
client = DataSourceAPIClient(
|
client = DataSourceAPIClient(base_url=base_url, token=token)
|
||||||
base_url=base_url,
|
|
||||||
token=token
|
|
||||||
)
|
|
||||||
return client.test_sql_with_schema(request_data)
|
return client.test_sql_with_schema(request_data)
|
||||||
else:
|
|
||||||
return default_client.test_sql_with_schema(request_data)
|
return default_client.test_sql_with_schema(request_data)
|
||||||
|
|
||||||
|
|
||||||
def process_skill_response(response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
def process_skill_response(response: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
"""
|
"""Map backend skill response into business query configs."""
|
||||||
处理API响应数据,映射为businessQueries格式
|
|
||||||
|
|
||||||
Args:
|
|
||||||
response: API原始响应数据
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
处理后的查询配置列表
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
# 提取data数组
|
|
||||||
data_list = response.get("data", [])
|
data_list = response.get("data", [])
|
||||||
|
|
||||||
# 默认的员工ID参数schema
|
|
||||||
default_employee_schema = {
|
|
||||||
"type": "object",
|
|
||||||
"required": ["employeeId"],
|
|
||||||
"properties": {
|
|
||||||
"employeeId": {
|
|
||||||
"type": "number",
|
|
||||||
"description": "员工ID,用于标识员工的唯一数字标识符",
|
|
||||||
"examples": [1001, 2002]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# 映射每个skill为businessQuery格式
|
|
||||||
queries = []
|
queries = []
|
||||||
for skill in data_list:
|
|
||||||
# 解析sqlParams字符串为JSON对象
|
|
||||||
sql_params_str = skill.get("sqlParams") or "{}"
|
|
||||||
sql_params = json.loads(sql_params_str)
|
|
||||||
|
|
||||||
# 判断sqlParams是否为空对象
|
for skill in data_list:
|
||||||
is_empty_params = (
|
sql_params_raw = skill.get("sqlParams")
|
||||||
not sql_params.get("properties") or
|
sql_params: Dict[str, Any] = {}
|
||||||
len(sql_params.get("properties", {})) == 0
|
|
||||||
) and (
|
if isinstance(sql_params_raw, dict):
|
||||||
not sql_params.get("required") or
|
sql_params = sql_params_raw
|
||||||
len(sql_params.get("required", [])) == 0
|
elif isinstance(sql_params_raw, str) and sql_params_raw.strip():
|
||||||
|
try:
|
||||||
|
parsed_sql_params = json.loads(sql_params_raw)
|
||||||
|
if isinstance(parsed_sql_params, dict):
|
||||||
|
sql_params = parsed_sql_params
|
||||||
|
else:
|
||||||
|
logger.warning(
|
||||||
|
f"技能 {skill.get('name')} (ID: {skill.get('id')}) 的 sqlParams 不是对象,已回退为空对象"
|
||||||
|
)
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
logger.warning(
|
||||||
|
f"技能 {skill.get('name')} (ID: {skill.get('id')}) 的 sqlParams 不是合法 JSON,已回退为空对象"
|
||||||
)
|
)
|
||||||
|
|
||||||
# # 如果是空对象,使用默认的员工ID参数
|
|
||||||
# if is_empty_params:
|
|
||||||
# logger.info(f"技能 {skill.get('name')} (ID: {skill.get('id')}) 的sqlParams为空,使用默认员工ID参数")
|
|
||||||
# sql_params = default_employee_schema
|
|
||||||
|
|
||||||
# 映射字段
|
|
||||||
query = {
|
query = {
|
||||||
"id": skill.get("id"),
|
"id": skill.get("id"),
|
||||||
"businessName": skill.get("name"),
|
"businessName": skill.get("name"),
|
||||||
"businessDescription": skill.get("description"),
|
"businessDescription": skill.get("description"),
|
||||||
"sqlTemplate": skill.get("sqlTemplate"),
|
"sqlTemplate": skill.get("sqlTemplate"),
|
||||||
"parameters": sql_params,
|
"parameters": sql_params,
|
||||||
"datasourceId": skill.get("datasourceId")
|
"datasourceId": skill.get("datasourceId"),
|
||||||
}
|
}
|
||||||
queries.append(query)
|
queries.append(query)
|
||||||
|
|
||||||
logger.info(f"成功处理 {len(queries)} 条技能数据")
|
logger.info(f"成功处理 {len(queries)} 条技能数据")
|
||||||
return queries
|
return queries
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"处理API响应数据失败: {e}", exc_info=True)
|
logger.error(f"处理 API 响应数据失败: {e}", exc_info=True)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
"""环境变量配置模块"""
|
"""环境变量配置模块"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
def get_database_id(default: str = "29") -> str:
|
def get_database_id(default: str = "29") -> str:
|
||||||
|
|||||||
@@ -1,77 +1,43 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
Schema 生成工具模块
|
Schema 生成工具模块。
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from typing import Any, Dict, List
|
from copy import deepcopy
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
def generate_input_schema(parameters: Dict[str, Any]) -> Dict[str, Any]:
|
def _normalize_schema(parameters: Dict[str, Any] | None) -> Dict[str, Any]:
|
||||||
"""
|
"""将任意参数定义归一化为 object schema。"""
|
||||||
从查询配置的参数定义生成 MCP 工具的 inputSchema
|
if not isinstance(parameters, dict) or not parameters:
|
||||||
|
|
||||||
此函数会保留完整的 JSON Schema 信息,包括:
|
|
||||||
- type: Schema 类型(通常是 "object")
|
|
||||||
- required: 必填字段列表
|
|
||||||
- properties: 属性定义(包括每个属性的 type, description, format, examples 等)
|
|
||||||
- description: Schema 的整体描述(如果有)
|
|
||||||
- 以及其他任何 JSON Schema 标准字段
|
|
||||||
|
|
||||||
此函数还会自动添加以下字段(如果原始 parameters 中未定义):
|
|
||||||
- targetDatabaseName: 目标数据库名称(非必填,默认为空字符串)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
parameters: 查询配置中的参数定义字典,应该是一个完整的 JSON Schema 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Dict[str, Any]: 符合 JSON Schema 规范的 inputSchema 对象
|
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> params = {
|
|
||||||
... "type": "object",
|
|
||||||
... "required": ["userId", "startTime"],
|
|
||||||
... "properties": {
|
|
||||||
... "userId": {
|
|
||||||
... "type": "integer",
|
|
||||||
... "description": "用户的唯一标识符",
|
|
||||||
... "examples": [10086]
|
|
||||||
... },
|
|
||||||
... "startTime": {
|
|
||||||
... "type": "string",
|
|
||||||
... "format": "date-time",
|
|
||||||
... "description": "查询的起始时间",
|
|
||||||
... "examples": ["2023-01-01 00:00:00"]
|
|
||||||
... }
|
|
||||||
... }
|
|
||||||
... }
|
|
||||||
>>> schema = generate_input_schema(params)
|
|
||||||
>>> # schema 将包含所有原始信息,包括 format 和 examples
|
|
||||||
>>> # 同时会自动添加 targetDatabaseName 字段
|
|
||||||
"""
|
|
||||||
# 如果 parameters 本身就是一个完整的 JSON Schema 对象,直接使用
|
|
||||||
# 但确保至少包含 type 和 properties
|
|
||||||
if not parameters:
|
|
||||||
# 如果 parameters 为空,返回一个空的 object schema
|
|
||||||
return {
|
return {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {},
|
"properties": {},
|
||||||
"required": []
|
"required": []
|
||||||
}
|
}
|
||||||
|
|
||||||
# 深拷贝 parameters 以避免修改原始数据
|
input_schema = deepcopy(parameters)
|
||||||
input_schema = dict(parameters)
|
|
||||||
|
|
||||||
# 确保必需的字段存在
|
|
||||||
if "type" not in input_schema:
|
|
||||||
input_schema["type"] = "object"
|
input_schema["type"] = "object"
|
||||||
|
|
||||||
if "properties" not in input_schema:
|
if not isinstance(input_schema.get("properties"), dict):
|
||||||
input_schema["properties"] = {}
|
input_schema["properties"] = {}
|
||||||
|
|
||||||
if "required" not in input_schema:
|
if not isinstance(input_schema.get("required"), list):
|
||||||
input_schema["required"] = []
|
input_schema["required"] = []
|
||||||
|
|
||||||
# 添加 targetDatabaseName 字段(如果不存在)
|
return input_schema
|
||||||
|
|
||||||
|
|
||||||
|
def generate_input_schema(parameters: Dict[str, Any] | None) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
从查询配置的参数定义生成 MCP 工具的 inputSchema。
|
||||||
|
|
||||||
|
会统一补齐:
|
||||||
|
- `targetDatabaseName`:可选
|
||||||
|
- `userId`:必填
|
||||||
|
"""
|
||||||
|
input_schema = _normalize_schema(parameters)
|
||||||
|
|
||||||
if "targetDatabaseName" not in input_schema["properties"]:
|
if "targetDatabaseName" not in input_schema["properties"]:
|
||||||
input_schema["properties"]["targetDatabaseName"] = {
|
input_schema["properties"]["targetDatabaseName"] = {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@@ -79,32 +45,21 @@ def generate_input_schema(parameters: Dict[str, Any]) -> Dict[str, Any]:
|
|||||||
"default": ""
|
"default": ""
|
||||||
}
|
}
|
||||||
|
|
||||||
# 保留所有其他字段,如 description, examples, format 等
|
if "userId" not in input_schema["properties"]:
|
||||||
# JSON Schema 标准支持的字段都会被保留:
|
input_schema["properties"]["userId"] = {
|
||||||
# - additionalProperties
|
"type": "string",
|
||||||
# - patternProperties
|
"description": "当前 AI 平台用户 ID"
|
||||||
# - minProperties / maxProperties
|
}
|
||||||
# - dependencies
|
|
||||||
# - 等等
|
if "userId" not in input_schema["required"]:
|
||||||
|
input_schema["required"].append("userId")
|
||||||
|
|
||||||
return input_schema
|
return input_schema
|
||||||
|
|
||||||
|
|
||||||
def validate_input_schema(schema: Dict[str, Any]) -> tuple[bool, str]:
|
def validate_input_schema(schema: Dict[str, Any]) -> tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
验证 inputSchema 是否符合基本的 JSON Schema 规范
|
验证 inputSchema 是否符合基本的 JSON Schema 规范。
|
||||||
|
|
||||||
Args:
|
|
||||||
schema: 要验证的 schema 对象
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tuple[bool, str]: (是否有效, 错误消息或成功消息)
|
|
||||||
|
|
||||||
Example:
|
|
||||||
>>> schema = {"type": "object", "properties": {"id": {"type": "string"}}}
|
|
||||||
>>> is_valid, msg = validate_input_schema(schema)
|
|
||||||
>>> print(is_valid, msg)
|
|
||||||
True, "Schema 验证通过"
|
|
||||||
"""
|
"""
|
||||||
if not isinstance(schema, dict):
|
if not isinstance(schema, dict):
|
||||||
return False, "Schema 必须是一个字典对象"
|
return False, "Schema 必须是一个字典对象"
|
||||||
@@ -118,19 +73,16 @@ def validate_input_schema(schema: Dict[str, Any]) -> tuple[bool, str]:
|
|||||||
if not isinstance(schema.get("properties"), dict):
|
if not isinstance(schema.get("properties"), dict):
|
||||||
return False, "Schema 的 properties 字段必须是一个字典对象"
|
return False, "Schema 的 properties 字段必须是一个字典对象"
|
||||||
|
|
||||||
# 验证 required 字段(如果存在)
|
|
||||||
if "required" in schema:
|
if "required" in schema:
|
||||||
required = schema["required"]
|
required = schema["required"]
|
||||||
if not isinstance(required, list):
|
if not isinstance(required, list):
|
||||||
return False, "Schema 的 required 字段必须是一个列表"
|
return False, "Schema 的 required 字段必须是一个列表"
|
||||||
|
|
||||||
# 验证所有 required 的字段都在 properties 中定义
|
|
||||||
properties = schema["properties"]
|
properties = schema["properties"]
|
||||||
for field in required:
|
for field in required:
|
||||||
if field not in properties:
|
if field not in properties:
|
||||||
return False, f"必填字段 '{field}' 未在 properties 中定义"
|
return False, f"必填字段 '{field}' 未在 properties 中定义"
|
||||||
|
|
||||||
# 验证 properties 中每个字段的定义
|
|
||||||
for prop_name, prop_def in schema["properties"].items():
|
for prop_name, prop_def in schema["properties"].items():
|
||||||
if not isinstance(prop_def, dict):
|
if not isinstance(prop_def, dict):
|
||||||
return False, f"属性 '{prop_name}' 的定义必须是一个字典对象"
|
return False, f"属性 '{prop_name}' 的定义必须是一个字典对象"
|
||||||
@@ -139,4 +91,3 @@ def validate_input_schema(schema: Dict[str, Any]) -> tuple[bool, str]:
|
|||||||
return False, f"属性 '{prop_name}' 必须包含 type 字段"
|
return False, f"属性 '{prop_name}' 必须包含 type 字段"
|
||||||
|
|
||||||
return True, "Schema 验证通过"
|
return True, "Schema 验证通过"
|
||||||
|
|
||||||
|
|||||||
@@ -1,14 +1,20 @@
|
|||||||
"""
|
"""
|
||||||
Entry point for lzwcai-mcp-sqlexecutor
|
Repository-local launcher for lzwcai-mcp-sqlexecutor.
|
||||||
Runs the MCP server for SQL query execution
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
|
||||||
os.environ["databaseId"] = "162"
|
|
||||||
os.environ["skillId"] = "2008360664955854850"
|
|
||||||
os.environ["backendBaseUrl"] = "http://192.168.2.236:8088"
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# Import and run the actual MCP server
|
|
||||||
from lzwcai_mcp_sqlexecutor.main import main
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# Keep local developer defaults without overriding explicit environment settings.
|
||||||
|
os.environ.setdefault("databaseId", "240")
|
||||||
|
os.environ.setdefault("skillId", "2058819964077572098")
|
||||||
|
os.environ.setdefault("backendBaseUrl", "http://192.168.2.236:8088")
|
||||||
|
|
||||||
|
from lzwcai_mcp_sqlexecutor.main import main as package_main
|
||||||
|
|
||||||
|
package_main()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "lzwcai-mcp-sqlexecutor"
|
name = "lzwcai-mcp-sqlexecutor"
|
||||||
version = "0.1.11"
|
version = "0.1.13"
|
||||||
description = "MCP server for executing business SQL queries with dynamic tool generation"
|
description = "MCP server for executing business SQL queries with dynamic tool generation"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user