feat(lzwcai-agile-db): 更新AgileDB技能至v0.4.2版本并扩展工具集
- 将技能版本从0.2.0升级至0.4.2 - 工具数量从33个扩展至57个,新增数据源管理、AI训练、库表关联配置等功能 - 新增MQTT字段关联同步模块(8个工具)和库表关联配置(3个工具) - 添加重要的契约提示和安全确认原则,包括target默认值、alter_table操作限制等 - 修正工具参数说明,如execute_sql的executableSql改为sql,参数结构优化 - 增强安全机制,明确危险操作的用户确认流程和目标资源选择规则 - 更新README.md中的工具数量统计和功能描述
This commit is contained in:
Binary file not shown.
@@ -1,7 +1,8 @@
|
|||||||
---
|
---
|
||||||
name: lzwcai-agile-db
|
name: lzwcai-agile-db
|
||||||
description: AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。
|
description: AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。
|
||||||
version: 0.2.0
|
metadata:
|
||||||
|
version: 0.4.2
|
||||||
---
|
---
|
||||||
|
|
||||||
# lzwcai-agile-db
|
# lzwcai-agile-db
|
||||||
@@ -9,33 +10,56 @@ version: 0.2.0
|
|||||||
AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。
|
AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。
|
||||||
|
|
||||||
|
|
||||||
## 完整工具清单(33 个工具)
|
## 完整工具清单(57 个工具)
|
||||||
|
|
||||||
本 skill 基于 `lzwcai_mcp_agile_db` MCP Server,共提供 33 个工具,分为 9 大类:
|
本 skill 基于 `lzwcai_mcp_agile_db` MCP Server,共提供 57 个工具,分为 12 大类。
|
||||||
|
|
||||||
### 一、数据源管理(6 个工具)
|
> **⚠️ 契约提示(2026-06 真机验证)**:以下契约文档曾写错、已按真机修正,调用时务必遵守:
|
||||||
|
> - **`target` 默认 `test`**(不是 prod)。写线上库必须显式传 `target="prod"`。
|
||||||
|
> - **`alter_table` 的 operation 只有 3 种**:`ADD_COLUMN` / `MODIFY_COLUMN` / `DROP_COLUMN`。其余(RENAME_COLUMN/ALTER_COLUMN_TYPE/SET_NOT_NULL...)后端**不支持**。
|
||||||
|
> - **`alter_database` 改名字段是 `newName`**(不是 newDatabaseName)。
|
||||||
|
> - **`generate_table_by_description` 是异步**:返回 taskId,需用 `get_ai_training_detail` 轮询。
|
||||||
|
> - **`list_tables_with_ai` 的 `datasourceId` 实为「库/配置 ID」**(list_databases 返回的 config id),不是连接 ID。
|
||||||
|
|
||||||
|
### 一、数据源管理(10 个工具)
|
||||||
|
|
||||||
| 工具 | 功能 | 危险等级 |
|
| 工具 | 功能 | 危险等级 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| `list_datasources` | 获取数据源列表 | 安全 |
|
| `list_datasources` | 获取数据源列表 | 安全 |
|
||||||
| `get_datasource_detail` | 获取数据源详情(含数据库、表结构) | 安全 |
|
| `get_datasource_detail` | 获取数据源详情(含数据库、表结构) | 安全 |
|
||||||
| `create_datasource` | 创建外部数据源连接 | 安全 |
|
| `create_datasource` | 创建外部数据源连接(可选先测连接) | 安全 |
|
||||||
| `update_datasource` | 更新数据源连接信息 | 中等 |
|
| `update_datasource` | 更新数据源连接信息 | 中等 |
|
||||||
| `toggle_datasource_status` | 启用/停用数据源 | 中等 |
|
| `toggle_datasource_status` | 启用/停用数据源 | 中等 |
|
||||||
| `delete_datasource` | 删除数据源 | **危险** |
|
| `delete_datasource` | 删除数据源 | **危险** |
|
||||||
|
| `test_connection` | 测试外部数据库连接(不创建) | 安全 |
|
||||||
|
| `get_realtime_structure` | 实时探测远端库表结构 | 安全 |
|
||||||
|
| `create_builtin_datasource` | 创建内置 PostgreSQL 连接(建库表链路第一步) | 安全 |
|
||||||
|
| `update_builtin_datasource` | 修改内置 PostgreSQL 连接 | 中等 |
|
||||||
|
|
||||||
### 二、数据库与表管理(6 个工具)
|
### 二、数据库与表管理(9 个工具)
|
||||||
|
|
||||||
| 工具 | 功能 | 危险等级 |
|
| 工具 | 功能 | 危险等级 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| `list_databases` | 获取数据源下的数据库列表 | 安全 |
|
| `list_databases` | 获取数据源下的数据库列表 | 安全 |
|
||||||
| `list_tables` | 获取数据源下的表列表 | 安全 |
|
| `list_tables` | 获取数据源下的表列表(基础) | 安全 |
|
||||||
|
| `list_tables_with_ai` | 获取表元数据列表(含 AI 训练描述,内置库主列表) | 安全 |
|
||||||
| `get_table_detail` | 获取表结构详情(字段、类型、主键) | 安全 |
|
| `get_table_detail` | 获取表结构详情(字段、类型、主键) | 安全 |
|
||||||
| `create_table` | 创建新表 | **危险** |
|
| `create_database` | 在内置源下创建数据库(建表前置) | **危险** |
|
||||||
| `alter_table` | 修改表结构 | **危险** |
|
| `create_table` | 在指定库创建新表 | **危险** |
|
||||||
| `generate_table_by_description` | AI 根据自然语言生成表结构 | 安全(仅生成,不创建) |
|
| `create_database_table` | 一次性建库+表(合并接口) | **危险** |
|
||||||
|
| `alter_table` | 修改表结构(仅 ADD/MODIFY/DROP_COLUMN) | **危险** |
|
||||||
|
| `alter_database` | 修改数据库(改名,字段 newName) | **危险** |
|
||||||
|
|
||||||
### 三、表数据 CRUD(5 个工具)
|
### 三、AI 训练与生成(4 个工具)
|
||||||
|
|
||||||
|
| 工具 | 功能 | 危险等级 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `generate_table_by_description` | AI 自然语言生成表结构(异步,返回 taskId) | 安全(仅生成,不创建) |
|
||||||
|
| `get_ai_training_detail` | 查询/轮询 AI 训练任务详情 | 安全 |
|
||||||
|
| `list_ai_trainings` | AI 补全训练任务列表 | 安全 |
|
||||||
|
| `create_ai_training_by_selected` | 按选中表创建 AI 补全训练 | 中等 |
|
||||||
|
|
||||||
|
### 四、表数据 CRUD(5 个工具,target 默认 test)
|
||||||
|
|
||||||
| 工具 | 功能 | 危险等级 |
|
| 工具 | 功能 | 危险等级 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
@@ -45,26 +69,26 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
| `delete_table_rows` | 删除数据行(按主键) | **危险** |
|
| `delete_table_rows` | 删除数据行(按主键) | **危险** |
|
||||||
| `export_table_excel` | 导出表数据为 Excel(base64) | 安全 |
|
| `export_table_excel` | 导出表数据为 Excel(base64) | 安全 |
|
||||||
|
|
||||||
### 四、SQL 执行(1 个工具)
|
### 五、SQL 执行(1 个工具)
|
||||||
|
|
||||||
| 工具 | 功能 | 危险等级 |
|
| 工具 | 功能 | 危险等级 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| `execute_sql` | 执行原生 SQL 查询 | 中等/危险 |
|
| `execute_sql` | 执行原生 SQL 查询(入参 `sql`,target 默认 prod) | 中等/危险 |
|
||||||
|
|
||||||
### 五、数据导入(2 个工具)
|
### 六、数据导入(2 个工具)
|
||||||
|
|
||||||
| 工具 | 功能 | 危险等级 |
|
| 工具 | 功能 | 危险等级 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| `preview_import_data` | 上传 Excel 文件,AI 智能识别并预览 | 安全 |
|
| `preview_import_data` | 从 URL 下载 Excel 文件,AI 智能识别并预览 | 安全 |
|
||||||
| `confirm_import_data` | 确认导入 AI 识别后的数据 | **危险** |
|
| `confirm_import_data` | 确认导入(需传 databaseName,自动组装结构) | **危险** |
|
||||||
|
|
||||||
### 六、表订阅(1 个工具)
|
### 七、表订阅(1 个工具)
|
||||||
|
|
||||||
| 工具 | 功能 | 危险等级 |
|
| 工具 | 功能 | 危险等级 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
| `toggle_table_subscription` | 切换表的订阅状态 | 中等 |
|
| `toggle_table_subscription` | 切换表的订阅状态(configId+tableName+subscribe) | 中等 |
|
||||||
|
|
||||||
### 七、API 密钥管理(6 个工具)
|
### 八、API 密钥管理(7 个工具)
|
||||||
|
|
||||||
| 工具 | 功能 | 危险等级 |
|
| 工具 | 功能 | 危险等级 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
@@ -74,8 +98,9 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
| `delete_api_key` | 删除 API 密钥 | **危险** |
|
| `delete_api_key` | 删除 API 密钥 | **危险** |
|
||||||
| `get_api_key_permissions` | 查看指定密钥的权限配置 | 安全 |
|
| `get_api_key_permissions` | 查看指定密钥的权限配置 | 安全 |
|
||||||
| `grant_api_key_permissions` | 批量为 API 密钥授予权限 | **危险** |
|
| `grant_api_key_permissions` | 批量为 API 密钥授予权限 | **危险** |
|
||||||
|
| `revoke_api_key_permissions` | 撤销/删除已授予的权限(按权限记录 ID) | **危险** |
|
||||||
|
|
||||||
### 八、技能与工具管理(6 个工具)
|
### 九、技能与工具管理(7 个工具)
|
||||||
|
|
||||||
| 工具 | 功能 | 危险等级 |
|
| 工具 | 功能 | 危险等级 |
|
||||||
|------|------|----------|
|
|------|------|----------|
|
||||||
@@ -84,7 +109,29 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
| `create_skill` | 为数据源创建技能 | 中等 |
|
| `create_skill` | 为数据源创建技能 | 中等 |
|
||||||
| `create_sql_tool` | 将 SQL 查询创建为可复用工具 | 中等 |
|
| `create_sql_tool` | 将 SQL 查询创建为可复用工具 | 中等 |
|
||||||
| `delete_skill_tool` | 删除技能下的工具 | **危险** |
|
| `delete_skill_tool` | 删除技能下的工具 | **危险** |
|
||||||
| `update_skill_config` | 更新技能配置 | 中等 |
|
| `update_skill_config` | 更新技能配置(名称/描述/模板) | 中等 |
|
||||||
|
| `update_skill_tool` | 修改技能工具(id+description+uniqueName) | 中等 |
|
||||||
|
|
||||||
|
### 十、库表关联配置(3 个工具,外置源纳管/内置删库)
|
||||||
|
|
||||||
|
| 工具 | 功能 | 危险等级 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `create_connection_config` | 建库表关联(外置源建连后选库选表的最后一步) | 中等 |
|
||||||
|
| `update_connection_config` | 改库表关联范围 | 中等 |
|
||||||
|
| `delete_connection_config` | 删库表关联(也是「删内置库」的实现) | **危险** |
|
||||||
|
|
||||||
|
### 十一、MQTT 字段关联同步(8 个工具,内置源专属)
|
||||||
|
|
||||||
|
| 工具 | 功能 | 危险等级 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `list_mqtt_configs` | MQTT 同步配置列表 | 安全 |
|
||||||
|
| `get_mqtt_config_detail` | 配置详情 | 安全 |
|
||||||
|
| `list_mqtt_target_tables` | 可同步目标表列表 | 安全 |
|
||||||
|
| `list_mqtt_target_table_columns` | 目标表字段列表 | 安全 |
|
||||||
|
| `create_mqtt_config` | 新增同步配置 | 中等 |
|
||||||
|
| `update_mqtt_config` | 修改同步配置 | 中等 |
|
||||||
|
| `delete_mqtt_config` | 删除同步配置 | **危险** |
|
||||||
|
| `refresh_mqtt_config_cache` | 刷新配置缓存 | 安全 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -103,8 +150,11 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
|
|
||||||
### 环境参数 `target`
|
### 环境参数 `target`
|
||||||
|
|
||||||
- `prod` = 生产环境(正式数据,默认值)
|
- `prod` = 生产环境(正式数据)
|
||||||
- `test` = 测试环境(测试数据)
|
- `test` = 测试环境(调试数据)
|
||||||
|
|
||||||
|
> **重要**:表数据 CRUD(query/insert/update/delete/export)的 `target` **默认 `test`**(安全优先)。要操作正式数据必须**显式传 `target="prod"`**。
|
||||||
|
> 例外:`execute_sql` 的 target 默认 `prod`(沿用原行为)。
|
||||||
|
|
||||||
### 主键 `primaryKey`
|
### 主键 `primaryKey`
|
||||||
|
|
||||||
@@ -120,6 +170,20 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
|
|
||||||
## ⚠️ 安全确认原则(必须遵守)
|
## ⚠️ 安全确认原则(必须遵守)
|
||||||
|
|
||||||
|
### 〇、目标资源不得擅自选择(最高优先级铁律)
|
||||||
|
|
||||||
|
定位操作目标的参数(`datasourceId` / `connectionId` / `databaseName` / `tableId` / `tableName` / `apiKeyId` / `skillId` 等),**永远不得猜测、编造或"随手挑一个"**:
|
||||||
|
|
||||||
|
- **多个候选** → 列出全部,让用户选;严禁默认第一个或自认为"最合理"的那个。
|
||||||
|
- **唯一候选** → 先声明"我将使用 XXX",给用户否决机会;创建/写入/删除类仍需用户确认后执行。
|
||||||
|
- **没有候选 / 信息不足** → 直接问用户,不得编造。
|
||||||
|
|
||||||
|
> 当你发现自己正在"替用户决定用哪个数据源/数据库/表"时,立刻停下来改成提问。
|
||||||
|
|
||||||
|
**创建类操作**(`create_table` / `create_datasource` 等)执行前必须逐项确认落点,绝不"随便找个数据源就建"。例如建表须确认:数据源(`connectionId`)+ 数据库(`databaseName`)+ 表名(`tableName`),三者都要用户点头。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 一、执行工具前的风险评估
|
### 一、执行工具前的风险评估
|
||||||
|
|
||||||
当执行以下类型的操作时,**必须先询问用户确认**,不得擅作主张:
|
当执行以下类型的操作时,**必须先询问用户确认**,不得擅作主张:
|
||||||
@@ -294,26 +358,36 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
**用户**: "查一下 users 表前 10 条数据"
|
**用户**: "查一下 users 表前 10 条数据"
|
||||||
|
|
||||||
```
|
```
|
||||||
调用: query_table_data(tableId="5", pageNum=1, pageSize=10)
|
调用: query_table_data(tableId="5", pageNum=1, pageSize=10) // target 默认 test;查正式数据传 target="prod"
|
||||||
返回: {
|
返回: {
|
||||||
"columns": [{"name": "id", "type": "INTEGER"}, {"name": "username", "type": "VARCHAR"}, ...],
|
"data": {
|
||||||
"data": [{"id": 1, "username": "admin", "email": "admin@test.com"}, ...],
|
"tables": {
|
||||||
"total": 156
|
"tableName": "users",
|
||||||
|
"columns": [
|
||||||
|
{"columnName": "id", "columnType": "serial", "isPrimaryKey": true},
|
||||||
|
{"columnName": "username", "columnType": "varchar"}, ...
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"content": [[1, "admin", "admin@test.com"], [2, "user1", "user1@test.com"], ...],
|
||||||
|
"total": 156
|
||||||
|
}
|
||||||
}
|
}
|
||||||
回复: users 表共 156 条记录,当前显示第 1-10 条:
|
回复: users 表共 156 条记录,当前显示第 1-10 条:
|
||||||
| id | username | email | created_at |
|
| id | username | email |
|
||||||
|----|----------|-----------------|---------------------|
|
|----|----------|-----------------|
|
||||||
| 1 | admin | admin@test.com | 2024-01-15 10:30:00 |
|
| 1 | admin | admin@test.com |
|
||||||
| 2 | user1 | user1@test.com | 2024-01-16 14:20:00 |
|
| 2 | user1 | user1@test.com |
|
||||||
...
|
...
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **返回结构注意**:列定义在 `data.tables.columns[]`(字段名 `columnName`、是否主键 `isPrimaryKey`),行数据在 `data.content[]` 且是**位置数组**(按列顺序,不是键值对象)。展示/取主键时要把 content 的每个位置对到 columns 的同序字段。
|
||||||
|
|
||||||
### 注意事项
|
### 注意事项
|
||||||
- `target` 参数:`prod`=生产环境(默认),`test`=测试环境
|
- `target` 参数:`test`=测试环境(**默认**),`prod`=生产环境(查正式数据须显式传)
|
||||||
- 如果用户未指定数量,默认 `pageSize=10`
|
- 如果用户未指定数量,默认 `pageSize=10`
|
||||||
- 如果用户说"翻页",增加 `pageNum` 参数
|
- 如果用户说"翻页",增加 `pageNum` 参数
|
||||||
- 如果用户想看更多数据,可以增大 `pageSize`(最大根据 API 限制)
|
- 如果用户想看更多数据,可以增大 `pageSize`(最大根据 API 限制)
|
||||||
- 数据返回格式为对象数组
|
- 行数据是位置数组(`data.content[]`),需对照 `data.tables.columns[]` 的列顺序解析
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -329,7 +403,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
1. 如果用户直接提供 SQL,直接使用
|
1. 如果用户直接提供 SQL,直接使用
|
||||||
如果用户提供自然语言需求,先转换为 SQL
|
如果用户提供自然语言需求,先转换为 SQL
|
||||||
↓
|
↓
|
||||||
2. 调用 execute_sql(datasourceId="xx", executableSql="SELECT ...")
|
2. 调用 execute_sql(datasourceId="xx", sql="SELECT ...")
|
||||||
↓
|
↓
|
||||||
3. 展示查询结果
|
3. 展示查询结果
|
||||||
↓
|
↓
|
||||||
@@ -345,7 +419,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
|
|
||||||
调用: execute_sql(
|
调用: execute_sql(
|
||||||
datasourceId="58",
|
datasourceId="58",
|
||||||
executableSql="SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC"
|
sql="SELECT region, COUNT(*) as order_count FROM orders GROUP BY region ORDER BY order_count DESC"
|
||||||
)
|
)
|
||||||
返回: {
|
返回: {
|
||||||
"columns": [{"name": "region"}, {"name": "order_count"}],
|
"columns": [{"name": "region"}, {"name": "order_count"}],
|
||||||
@@ -362,7 +436,8 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
### 注意事项
|
### 注意事项
|
||||||
- `datasourceId` 必须提供,如果用户未指定,先询问
|
- `datasourceId` 必须提供,如果用户未指定,先询问
|
||||||
- `sqlTemplate` 可选,用于模板化查询
|
- `sqlTemplate` 可选,用于模板化查询
|
||||||
- `parameters` 可选,用于参数化查询(防止 SQL 注入)
|
- `params` 可选(对象),用于参数化查询(防止 SQL 注入);工具内部映射为后端的 `parameters`
|
||||||
|
- `target` 可选:`test`=测试环境(默认),`prod`=生产环境
|
||||||
- **执行危险操作(DELETE/DROP/TRUNCATE)前,必须向用户确认**
|
- **执行危险操作(DELETE/DROP/TRUNCATE)前,必须向用户确认**
|
||||||
- 如果查询结果超过 100 行,建议用户使用 `query_table_data` 代替
|
- 如果查询结果超过 100 行,建议用户使用 `query_table_data` 代替
|
||||||
- 对于只读查询(SELECT),可以直接执行;对于写操作(INSERT/UPDATE/DELETE),必须二次确认
|
- 对于只读查询(SELECT),可以直接执行;对于写操作(INSERT/UPDATE/DELETE),必须二次确认
|
||||||
@@ -497,7 +572,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
- `data` 只包含要更新的字段,不需要提供全部字段
|
- `data` 只包含要更新的字段,不需要提供全部字段
|
||||||
- 插入数据时,自增主键不需要提供
|
- 插入数据时,自增主键不需要提供
|
||||||
- 如果操作涉及多行,使用批量操作或循环调用
|
- 如果操作涉及多行,使用批量操作或循环调用
|
||||||
- 增删改操作默认作用于 `prod` 环境
|
- 增删改操作 `target` **默认 `test`**(安全优先);要写正式数据必须显式传 `target="prod"`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -511,12 +586,20 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
用户请求: "我需要一个用户表" / "帮我设计一个订单系统的表结构"
|
用户请求: "我需要一个用户表" / "帮我设计一个订单系统的表结构"
|
||||||
↓
|
↓
|
||||||
1. 调用 generate_table_by_description(requirement="用户描述")
|
1. 调用 generate_table_by_description(requirement="用户描述")
|
||||||
|
→ 返回 taskId(异步任务,此时还没有表结构)
|
||||||
↓
|
↓
|
||||||
2. 展示 AI 生成的表结构(表名、字段、类型、注释)
|
2. 轮询 get_ai_training_detail(taskId="xx") 直到 trainingStatus=2(已完成)
|
||||||
|
trainingStatus: 0 待训练 / 1 训练中 / 2 已完成 / 3 失败
|
||||||
|
完成后表结构在 createTableData.data.tables[] 里
|
||||||
↓
|
↓
|
||||||
3. 用户确认后,调用 create_table(connectionId="xx", databaseName="xx", tableName="xx", columns=[...])
|
3. 展示 AI 生成的表结构(表名、字段、类型、注释)
|
||||||
↓
|
↓
|
||||||
4. 确认创建成功
|
4. ⚠️ 确认落点:列出可用数据源,让用户选择 connectionId 和 databaseName
|
||||||
|
(多个数据源时必须让用户选,绝不擅自找一个;只有一个也要说明后再继续)
|
||||||
|
↓
|
||||||
|
5. 用户确认表结构 + 落点后,调用 create_table(connectionId="xx", databaseName="xx", tableName="xx", columns=[...])
|
||||||
|
↓
|
||||||
|
6. 确认创建成功
|
||||||
```
|
```
|
||||||
|
|
||||||
### 示例
|
### 示例
|
||||||
@@ -527,28 +610,34 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
调用: generate_table_by_description(
|
调用: generate_table_by_description(
|
||||||
requirement="我需要一个商品表,包含商品名称、价格、库存、分类,用于电商系统"
|
requirement="我需要一个商品表,包含商品名称、价格、库存、分类,用于电商系统"
|
||||||
)
|
)
|
||||||
返回: {
|
返回: {"data": {"taskId": "831", "status": "0", "message": "任务已提交,等待AI生成表结构"}}
|
||||||
"tableName": "products",
|
← 异步!返回的是 taskId,不是表结构
|
||||||
"tableComment": "商品表",
|
|
||||||
"columns": [
|
轮询: get_ai_training_detail(taskId="831")
|
||||||
{"columnName": "id", "columnType": "SERIAL", "isPrimaryKey": true, "isAutoIncrement": true, "columnComment": "主键ID"},
|
返回(完成时): {
|
||||||
{"columnName": "name", "columnType": "VARCHAR", "columnLength": 200, "isNullable": false, "columnComment": "商品名称"},
|
"data": {
|
||||||
{"columnName": "price", "columnType": "DECIMAL", "columnLength": 10, "columnComment": "价格"},
|
"trainingStatus": "2", // 2=已完成
|
||||||
{"columnName": "stock", "columnType": "INTEGER", "columnComment": "库存数量"},
|
"progress": 100,
|
||||||
{"columnName": "category_id", "columnType": "INTEGER", "columnComment": "分类ID"},
|
"createTableData": {"data": {"tables": [{
|
||||||
{"columnName": "created_at", "columnType": "TIMESTAMP", "defaultValue": "NOW()", "columnComment": "创建时间"}
|
"tableName": "products",
|
||||||
]
|
"tableComment": "商品表",
|
||||||
|
"columns": [
|
||||||
|
{"columnName": "id", "columnType": "SERIAL", "isPrimaryKey": true, "columnComment": "主键ID"},
|
||||||
|
{"columnName": "name", "columnType": "VARCHAR", "columnLength": 200, "isNullable": false, "columnComment": "商品名称"},
|
||||||
|
{"columnName": "price", "columnType": "DECIMAL", "columnLength": 10, "columnComment": "价格"},
|
||||||
|
{"columnName": "stock", "columnType": "INTEGER", "columnComment": "库存数量"}
|
||||||
|
]
|
||||||
|
}]}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
回复: AI 已生成表结构设计:
|
回复: AI 已生成表结构设计:
|
||||||
表名: products (商品表)
|
表名: products (商品表)
|
||||||
字段:
|
字段:
|
||||||
- id (SERIAL, 主键, 自增) - 主键ID
|
- id (SERIAL, 主键) - 主键ID
|
||||||
- name (VARCHAR(200), 非空) - 商品名称
|
- name (VARCHAR(200), 非空) - 商品名称
|
||||||
- price (DECIMAL(10)) - 价格
|
- price (DECIMAL(10)) - 价格
|
||||||
- stock (INTEGER) - 库存数量
|
- stock (INTEGER) - 库存数量
|
||||||
- category_id (INTEGER) - 分类ID
|
|
||||||
- created_at (TIMESTAMP, 默认NOW()) - 创建时间
|
|
||||||
|
|
||||||
是否需要在此基础上修改或直接创建?
|
是否需要在此基础上修改或直接创建?
|
||||||
|
|
||||||
@@ -559,7 +648,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
databaseName="order_db",
|
databaseName="order_db",
|
||||||
tableName="products",
|
tableName="products",
|
||||||
tableComment="商品表",
|
tableComment="商品表",
|
||||||
columns=[...] // 使用 AI 生成的 columns
|
columns=[...] // 使用轮询拿到的 createTableData.data.tables[0].columns
|
||||||
)
|
)
|
||||||
回复: 已成功创建表 products (商品表)
|
回复: 已成功创建表 products (商品表)
|
||||||
```
|
```
|
||||||
@@ -580,15 +669,13 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
4. 确认修改成功
|
4. 确认修改成功
|
||||||
```
|
```
|
||||||
|
|
||||||
**可用的 operations 类型**:
|
**可用的 operations 类型(真机验证,后端仅支持这 3 种)**:
|
||||||
- `ADD_COLUMN`:添加字段
|
- `ADD_COLUMN`:添加字段
|
||||||
|
- `MODIFY_COLUMN`:修改字段(改类型/长度/可空/默认值/注释,整列重定义)
|
||||||
- `DROP_COLUMN`:删除字段
|
- `DROP_COLUMN`:删除字段
|
||||||
- `RENAME_COLUMN`:重命名字段
|
|
||||||
- `ALTER_COLUMN_TYPE`:修改字段类型
|
> ⚠️ 后端**不支持** `RENAME_COLUMN`/`ALTER_COLUMN_TYPE`/`SET_NOT_NULL`/`DROP_NOT_NULL`/`SET_DEFAULT`/`DROP_DEFAULT`(会报「不支持的操作类型」)。改字段类型/约束统一用 `MODIFY_COLUMN` 传完整列定义。
|
||||||
- `SET_NOT_NULL`:设置为非空
|
> `operations[].column` 是列定义对象,字段同建表(columnName/columnType/columnLength/isNullable/columnComment/defaultValue 等)。
|
||||||
- `DROP_NOT_NULL`:取消非空约束
|
|
||||||
- `SET_DEFAULT`:设置默认值
|
|
||||||
- `DROP_DEFAULT`:删除默认值
|
|
||||||
|
|
||||||
### 注意事项
|
### 注意事项
|
||||||
- `requirement` 参数应尽可能详细,包含业务场景和字段需求
|
- `requirement` 参数应尽可能详细,包含业务场景和字段需求
|
||||||
@@ -608,9 +695,9 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
```
|
```
|
||||||
用户请求: "帮我导入这个 Excel 文件" / "把表格数据导入数据库"
|
用户请求: "帮我导入这个 Excel 文件" / "把表格数据导入数据库"
|
||||||
↓
|
↓
|
||||||
1. 用户将 Excel 文件转为 base64 编码
|
1. 拿到 Excel 文件的可下载 URL(http/https,<500KB,.xlsx/.xls)
|
||||||
↓
|
↓
|
||||||
2. 调用 preview_import_data(connectionId="xx", file_base64="...", file_name="data.xlsx", target="test")
|
2. 调用 preview_import_data(connectionId="xx", file_url="https://example.com/data.xlsx", target="test")
|
||||||
↓
|
↓
|
||||||
3. 展示 AI 识别的表结构和数据预览
|
3. 展示 AI 识别的表结构和数据预览
|
||||||
↓
|
↓
|
||||||
@@ -618,9 +705,10 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
说明:导入的数据将写入数据库,请确认数据来源合法合规,不包含敏感信息、政治内容或违规内容。
|
说明:导入的数据将写入数据库,请确认数据来源合法合规,不包含敏感信息、政治内容或违规内容。
|
||||||
请确认是否继续?
|
请确认是否继续?
|
||||||
↓
|
↓
|
||||||
5. 用户确认无误后,调用 confirm_import_data(connectionId="xx", data={...}, target="test")
|
5. 用户确认无误后,调用 confirm_import_data(connectionId="xx", databaseName="目标库名", data=<预览返回的原文>, target="test")
|
||||||
|
(把 preview_import_data 的返回原样传给 data,工具会自动组装后端要求的结构;databaseName 是落库的目标库)
|
||||||
↓
|
↓
|
||||||
6. 确认导入成功
|
6. 确认导入成功(返回含 insertedRows 插入行数)
|
||||||
```
|
```
|
||||||
|
|
||||||
### 注意事项
|
### 注意事项
|
||||||
@@ -628,7 +716,10 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
- 支持格式:.xlsx / .xls
|
- 支持格式:.xlsx / .xls
|
||||||
- 导入前默认使用 `test` 环境(安全做法)
|
- 导入前默认使用 `test` 环境(安全做法)
|
||||||
- 如果用户要导入到正式环境,必须二次确认
|
- 如果用户要导入到正式环境,必须二次确认
|
||||||
- base64 编码的文件内容需要提供文件名
|
- `file_url` 要求 http/https 可下载链接,且文件大小 < 500KB
|
||||||
|
- `file_url` 对应的文件扩展名需为 `.xlsx` / `.xls`
|
||||||
|
- **`confirm_import_data` 必须传 `databaseName`**(落库目标库);`data` 直接传 `preview_import_data` 的返回原文即可,工具内部会自动解包并组装成 `{tableStructure(含databaseName), allData}`
|
||||||
|
- AI 识别会把中文表头转成英文列名(如「姓名」→`name`);若导入数据键名与生成的列名对不上会报「未找到 XX 字段」,此时需按预览返回的列名核对
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -707,6 +798,23 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
- `database`:数据库级别权限
|
- `database`:数据库级别权限
|
||||||
- `table`:表级别权限
|
- `table`:表级别权限
|
||||||
|
|
||||||
|
### 7.7 撤销权限
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: get_api_key_permissions(apiKeyId="7")
|
||||||
|
返回: {
|
||||||
|
"data": {
|
||||||
|
"connectionPermissions": [{"id": "101", "connectionId": "58", "permissionType": "read"}],
|
||||||
|
"databasePermissions": [...],
|
||||||
|
"tablePermissions": [...]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
调用: revoke_api_key_permissions(permissionIds=["101"])
|
||||||
|
```
|
||||||
|
|
||||||
|
> 说明:`revoke_api_key_permissions` 按权限记录的 `id` 删除,需先从 `get_api_key_permissions` 获取。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 场景 8:技能与工具管理
|
## 场景 8:技能与工具管理
|
||||||
@@ -768,6 +876,20 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### 8.7 修改技能下某个工具
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: update_skill_tool(
|
||||||
|
id="2066468871151718402", // 工具 ID(来自 get_skill_tools 返回的 id)
|
||||||
|
description="改后的业务描述", // 可选
|
||||||
|
uniqueName="新展示名", // 可选(工具展示名)
|
||||||
|
sqlTemplate="SELECT ...", // 可选
|
||||||
|
resultType="list" // 可选:single / list
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
> 说明:后端实体真实字段为 `id`/`uniqueName`/`description`/`sqlTemplate`/`resultType`。旧参数名 `skillToolId`/`name`/`businessDescription` 仍有兼容映射,但建议直接用上面的真实字段名。`businessScenario` 后端无此字段,会被丢弃。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 场景 9:表订阅管理
|
## 场景 9:表订阅管理
|
||||||
@@ -778,13 +900,117 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
调用: toggle_table_subscription(
|
调用: toggle_table_subscription(
|
||||||
configId="数据库配置ID",
|
configId="数据库配置ID",
|
||||||
tableName="orders",
|
tableName="orders",
|
||||||
isSubscribe=true // true=订阅, false=取消订阅
|
subscribe=true // true=订阅, false=取消订阅
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 最佳实践
|
## 场景 10:库表关联配置(外置源纳管 / 内置删库)
|
||||||
|
|
||||||
|
「库表关联配置」是数据源能用于智能问数的**最后一步**:外置数据源建好连接后,必须选定要纳管的库与表,平台才会同步结构、允许后续问数。这一类有 3 个工具:`create_connection_config` / `update_connection_config` / `delete_connection_config`。
|
||||||
|
|
||||||
|
> **概念澄清**:
|
||||||
|
> - 配置(config)≠ 连接(connection)。`create_connection_config` 落库后产生的 **config id**,正是 `list_databases` / `list_tables_with_ai` 里要传的那个「库/配置 ID」(`datasourceId` 参数实际指它,不是连接 ID)。
|
||||||
|
> - `delete_connection_config` 既是「删库表关联」,也是**「删除内置库」的唯一实现**——删内置库就是删掉它对应的那条配置。
|
||||||
|
|
||||||
|
### 10.1 外置源纳管(建连接后的收尾步骤)
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求: "把刚建的 MySQL 数据源接进来" / "让这个数据源能问数"
|
||||||
|
↓
|
||||||
|
1. 已有外置连接(create_datasource 返回的 connectionId)
|
||||||
|
↓
|
||||||
|
2. 调用 get_realtime_structure(datasourceId="连接ID") 探测真实库表
|
||||||
|
↓
|
||||||
|
3. ⚠️ 列出探测到的库和表,让用户勾选要纳管的范围
|
||||||
|
(多个库/表时必须让用户选,不得擅自全选或挑第一个)
|
||||||
|
↓
|
||||||
|
4. 调用 create_connection_config(connectionId="xx", databases=[{databaseName, tableNames:[...]}])
|
||||||
|
↓
|
||||||
|
5. 确认纳管成功(返回 config id,后续 list_tables_with_ai 用它)
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
**用户**: "把 mysql_test 数据源的 order_db 库接进来,纳管 orders 和 users 两张表"
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: get_realtime_structure(datasourceId="58") // 先确认库表真实存在
|
||||||
|
调用: create_connection_config(
|
||||||
|
connectionId="58",
|
||||||
|
syncTables=true, // 默认 true,同步表结构
|
||||||
|
databases=[
|
||||||
|
{"databaseName": "order_db", "tableNames": ["orders", "users"]}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
回复: 已纳管 order_db 库的 orders、users 两张表,该数据源现在可用于智能问数。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 修改纳管范围
|
||||||
|
|
||||||
|
```
|
||||||
|
用户请求: "再把 products 表也纳管进来" / "改一下纳管的表"
|
||||||
|
↓
|
||||||
|
1. 先拿到该配置的 config id(list_databases 返回里的 id)
|
||||||
|
↓
|
||||||
|
2. ⚠️ 与用户确认最终要纳管的完整库表清单(这是全量覆盖,不是增量追加)
|
||||||
|
↓
|
||||||
|
3. 调用 update_connection_config(id="配置ID", connectionId="xx", databases=[...])
|
||||||
|
↓
|
||||||
|
4. 确认更新成功
|
||||||
|
```
|
||||||
|
|
||||||
|
> ⚠️ **全量覆盖语义**:`databases` 传的是更新后的**完整**纳管范围,不是增量。漏传已有的表会导致那些表被移出纳管,务必带上全部要保留的表。
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
```
|
||||||
|
调用: update_connection_config(
|
||||||
|
id="123", // 配置 ID(编辑必传)
|
||||||
|
connectionId="58",
|
||||||
|
databases=[
|
||||||
|
{"databaseName": "order_db", "tableNames": ["orders", "users", "products"]}
|
||||||
|
]
|
||||||
|
)
|
||||||
|
回复: 已更新纳管范围,order_db 现纳管 orders、users、products 三张表。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.3 删除配置(含删除内置库)
|
||||||
|
|
||||||
|
```
|
||||||
|
⚠️ 安全提醒:此操作存在删除风险。
|
||||||
|
说明:删除库表关联配置后,该库表将退出纳管、无法再问数;
|
||||||
|
若目标是内置库,则该库本身会被删除,库内数据永久丢失,不可恢复。
|
||||||
|
请确认是否继续?(回复"确认"继续,或取消操作)
|
||||||
|
↓
|
||||||
|
用户确认后,调用 delete_connection_config(ids=["配置ID1", "配置ID2"])
|
||||||
|
```
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
|
||||||
|
**用户**: "删掉那个测试内置库"
|
||||||
|
|
||||||
|
```
|
||||||
|
回复: ⚠️ 确认要删除内置库「test_db」(配置 ID=456)吗?
|
||||||
|
库内全部数据将永久删除,不可恢复。请回复"确认删除"继续。
|
||||||
|
|
||||||
|
用户: "确认删除"
|
||||||
|
|
||||||
|
调用: delete_connection_config(ids=["456"])
|
||||||
|
回复: 已删除配置 456(内置库 test_db)。
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
- `databases` 每项是 `{databaseName, tableNames:[...]}`,`tableNames` 是表名字符串数组(不是 tableId)
|
||||||
|
- `syncTables` 默认 `true`(纳管时同步表结构),一般无需手动传
|
||||||
|
- `update_connection_config` 必传 `id`(配置 ID),且 `databases` 为**全量**覆盖
|
||||||
|
- `delete_connection_config` 的 `ids` 是数组,支持批量删;删内置库即删其对应配置 ID
|
||||||
|
- 内置库删除属**危险操作**,务必先确认库名与影响后再执行
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### 1. 安全第一
|
### 1. 安全第一
|
||||||
|
|
||||||
@@ -839,8 +1065,8 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
- `1` - 已停止
|
- `1` - 已停止
|
||||||
|
|
||||||
### 环境
|
### 环境
|
||||||
- `prod` - 生产环境(默认)
|
- `test` - 测试环境(**表数据 CRUD 的默认值**,安全优先)
|
||||||
- `test` - 测试环境
|
- `prod` - 生产环境(操作正式数据须显式传 `target="prod"`;仅 `execute_sql` 默认 prod)
|
||||||
|
|
||||||
### 常用字段类型
|
### 常用字段类型
|
||||||
| 类型 | 用途 | 示例 |
|
| 类型 | 用途 | 示例 |
|
||||||
@@ -854,16 +1080,13 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
| `BOOLEAN` | 布尔值 | `is_active BOOLEAN` |
|
| `BOOLEAN` | 布尔值 | `is_active BOOLEAN` |
|
||||||
|
|
||||||
### 表结构变更操作类型(alter_table)
|
### 表结构变更操作类型(alter_table)
|
||||||
| 类型 | 用途 |
|
| 类型 | 用途 | column 对象关键字段 |
|
||||||
|------|------|
|
|------|------|------|
|
||||||
| `ADD_COLUMN` | 添加字段 |
|
| `ADD_COLUMN` | 添加字段 | columnName, columnType, columnLength, isNullable, columnComment |
|
||||||
| `DROP_COLUMN` | 删除字段 |
|
| `MODIFY_COLUMN` | 修改字段(类型/长度/约束等,含改类型) | columnName, columnType, columnLength, ... |
|
||||||
| `RENAME_COLUMN` | 重命名字段 |
|
| `DROP_COLUMN` | 删除字段 | columnName |
|
||||||
| `ALTER_COLUMN_TYPE` | 修改字段类型 |
|
|
||||||
| `SET_NOT_NULL` | 设置为非空 |
|
> 后端**只支持上述 3 种**。`RENAME_COLUMN`/`ALTER_COLUMN_TYPE`/`SET_NOT_NULL`/`DROP_NOT_NULL`/`SET_DEFAULT`/`DROP_DEFAULT` 均会报「不支持的操作类型」(已真机验证)。改字段类型用 `MODIFY_COLUMN`。
|
||||||
| `DROP_NOT_NULL` | 取消非空约束 |
|
|
||||||
| `SET_DEFAULT` | 设置默认值 |
|
|
||||||
| `DROP_DEFAULT` | 删除默认值 |
|
|
||||||
|
|
||||||
### 权限级别(API 密钥授权)
|
### 权限级别(API 密钥授权)
|
||||||
| 级别 | 范围 |
|
| 级别 | 范围 |
|
||||||
@@ -879,7 +1102,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
|||||||
如果用户说"帮我查一下数据库",按以下步骤操作:
|
如果用户说"帮我查一下数据库",按以下步骤操作:
|
||||||
|
|
||||||
1. 调用 `list_datasources()` 获取数据源列表
|
1. 调用 `list_datasources()` 获取数据源列表
|
||||||
2. 展示列表,让用户选择或默认第一个运行中的数据源
|
2. 展示列表,让用户选择目标数据源(**多个候选时必须让用户选,不得默认第一个**;只有一个候选也要先说明"我将使用 XXX"再继续)
|
||||||
3. 调用 `get_datasource_detail(datasourceId="xx")` 获取数据库和表信息
|
3. 调用 `get_datasource_detail(datasourceId="xx")` 获取数据库和表信息
|
||||||
4. 引导用户选择要操作的表
|
4. 引导用户选择要操作的表
|
||||||
5. 根据用户意图调用相应的工具:
|
5. 根据用户意图调用相应的工具:
|
||||||
|
|||||||
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
# lzwcai-mcp-agile-db
|
# lzwcai-mcp-agile-db
|
||||||
|
|
||||||
数据库管理平台 MCP Server,提供 33 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。
|
数据库管理平台 MCP Server,提供 34 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。
|
||||||
|
|
||||||
## 环境变量
|
## 环境变量
|
||||||
|
|
||||||
@@ -58,6 +58,7 @@ lzwcai-mcp-agile-db
|
|||||||
- `delete_api_key` - 删除密钥
|
- `delete_api_key` - 删除密钥
|
||||||
- `get_api_key_permissions` - 查看密钥权限
|
- `get_api_key_permissions` - 查看密钥权限
|
||||||
- `grant_api_key_permissions` - 授予权限
|
- `grant_api_key_permissions` - 授予权限
|
||||||
|
- `revoke_api_key_permissions` - 撤销/删除已授予权限
|
||||||
|
|
||||||
### 技能与工具管理
|
### 技能与工具管理
|
||||||
- `get_skill_by_datasource` - 获取技能信息
|
- `get_skill_by_datasource` - 获取技能信息
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -1,202 +1,10 @@
|
|||||||
2026-06-11 09:30:49 - 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-17 11:19:35 - 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-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
2026-06-17 11:19:35 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||||
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
2026-06-17 11:19:35 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||||
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
2026-06-17 11:19:35 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:124] - ==================================================
|
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=http://x
|
||||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
|
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:78] - [API响应] HTTP 400
|
||||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
|
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:98] - [API错误] HTTP 400, 参数不合法
|
||||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
|
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:78] - [API响应] HTTP 200
|
||||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
|
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:78] - [API响应] HTTP 404
|
||||||
2026-06-11 09:30:49 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:107] - [API错误] HTTP 404, {"path": "/x"}
|
||||||
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
|
||||||
2026-06-11 09:30:50 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x0000025426747BF0>
|
|
||||||
2026-06-11 09:30:50 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
|
||||||
2026-06-11 09:30:50 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
|
||||||
2026-06-11 09:30:50 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
|
||||||
2026-06-11 09:30:50 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
|
||||||
2026-06-11 09:33:24 - 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-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
|
||||||
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
|
||||||
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
|
||||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:124] - ==================================================
|
|
||||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
|
|
||||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
|
|
||||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
|
|
||||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
|
|
||||||
2026-06-11 09:33:24 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
|
||||||
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
|
||||||
2026-06-11 09:33:25 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x0000020155C7D190>
|
|
||||||
2026-06-11 09:33:25 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
|
||||||
2026-06-11 09:33:25 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
|
||||||
2026-06-11 09:33:25 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
|
||||||
2026-06-11 09:33:25 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=https://dempdemo.lzwcai.com
|
|
||||||
2026-06-11 09:33:25 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 33 个工具
|
|
||||||
2026-06-11 09:33:25 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
|
||||||
2026-06-11 09:33:45 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x0000020155844410>
|
|
||||||
2026-06-11 09:33:45 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
|
||||||
2026-06-11 09:33:45 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
|
||||||
2026-06-11 09:33:45 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_datasources
|
|
||||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/connection/list
|
|
||||||
2026-06-11 09:33:46 - httpx - DEBUG - [_config.py:82] - load_ssl_context verify=True cert=None trust_env=True http2=False
|
|
||||||
2026-06-11 09:33:46 - httpx - DEBUG - [_config.py:148] - load_verify_locations cafile='D:\\anaconda3\\Library\\ssl\\cacert.pem'
|
|
||||||
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
|
||||||
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000020155E9D8E0>
|
|
||||||
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000020155E740D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
|
||||||
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000020155D4CFE0>
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:33:40 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'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
|
||||||
2026-06-11 09:33:46 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/connection/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
|
|
||||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
|
|
||||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
|
|
||||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:91] - [API错误] 登录过期,请重新登录
|
|
||||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.server - ERROR - [server.py:96] - 工具执行失败: list_datasources, 错误: 登录过期,请重新登录
|
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\server.py", line 84, in handle_call_tool
|
|
||||||
result = await tool_instance.execute(arguments or {})
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\tools\datasources.py", line 30, in execute
|
|
||||||
return self.client.get("/api/datasource/connection/list", params=params)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 102, in get
|
|
||||||
return self._handle_response(response, url)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 92, in _handle_response
|
|
||||||
raise Exception(error_msg)
|
|
||||||
Exception: 登录过期,请重新登录
|
|
||||||
2026-06-11 09:33:46 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
|
||||||
2026-06-11 09:39:21 - 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-11 09:39:21 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
|
||||||
2026-06-11 09:39:21 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
|
||||||
2026-06-11 09:39:21 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
|
||||||
2026-06-11 09:39:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=https://dempdemo.lzwcai.com
|
|
||||||
2026-06-11 09:39:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/connection/list
|
|
||||||
2026-06-11 09:39:21 - httpx - DEBUG - [_config.py:82] - load_ssl_context verify=True cert=None trust_env=True http2=False
|
|
||||||
2026-06-11 09:39:21 - httpx - DEBUG - [_config.py:148] - load_verify_locations cafile='C:\\Users\\HiWin10\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\certifi\\cacert.pem'
|
|
||||||
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
|
||||||
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000027729C00710>
|
|
||||||
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000027729DFE9D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
|
||||||
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000027729C23170>
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:39:15 GMT'), (b'Content-Type', b'application/json'), (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'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
|
||||||
2026-06-11 09:39:21 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/connection/list "HTTP/1.1 200 "
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
|
|
||||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
|
|
||||||
2026-06-11 09:39:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
|
|
||||||
2026-06-11 09:39:52 - 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-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
|
||||||
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
|
||||||
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
|
||||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:124] - ==================================================
|
|
||||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
|
|
||||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
|
|
||||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
|
|
||||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
|
|
||||||
2026-06-11 09:39:52 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
|
||||||
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
|
||||||
2026-06-11 09:39:53 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002B57D06C860>
|
|
||||||
2026-06-11 09:39:53 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
|
||||||
2026-06-11 09:39:53 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
|
||||||
2026-06-11 09:39:53 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
|
||||||
2026-06-11 09:39:53 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=https://dempdemo.lzwcai.com
|
|
||||||
2026-06-11 09:39:53 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 33 个工具
|
|
||||||
2026-06-11 09:39:53 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
|
||||||
2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002B57D188A40>
|
|
||||||
2026-06-11 09:40:08 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
|
||||||
2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
|
||||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_datasources
|
|
||||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/connection/list
|
|
||||||
2026-06-11 09:40:08 - httpx - DEBUG - [_config.py:82] - load_ssl_context verify=True cert=None trust_env=True http2=False
|
|
||||||
2026-06-11 09:40:08 - httpx - DEBUG - [_config.py:148] - load_verify_locations cafile='D:\\anaconda3\\Library\\ssl\\cacert.pem'
|
|
||||||
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
|
||||||
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57D28C560>
|
|
||||||
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x000002B57D2642D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
|
||||||
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57CF80410>
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:40:02 GMT'), (b'Content-Type', b'application/json'), (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'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
|
||||||
2026-06-11 09:40:08 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/connection/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
|
|
||||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
|
|
||||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
|
|
||||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_datasources
|
|
||||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
|
||||||
"total": 14,
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"createBy": "",
|
|
||||||
"createTime": "2026-06-10 16:47:43",
|
|
||||||
"updateBy": "",
|
|
||||||
"updateTime": "2026-06-10 16:47:43",
|
|
||||||
"remark": "设备报价管理系统包含设备基础信息、预设方案模板、报价单主表和明细表四个核心数据对象,支持从设备参数管理到整套产线方案配置的完整报价流程,为销售部门提供标准化报价服务,实现快速方案生成和精准成本核算。",
|
|
||||||
"id": "58",
|
|
||||||
"enterpriseId": "1937166012193443842",
|
|
||||||
"deptId": "1171",
|
|
||||||
"userId": "292",
|
|
||||||
"host": "host.docker.internal",
|
|
||||||
"port": 5432,
|
|
||||||
"datasourceName": "HMD产品",
|
|
||||||
"database...
|
|
||||||
2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
|
||||||
2026-06-11 09:40:55 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002B57D0617C0>
|
|
||||||
2026-06-11 09:40:55 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
|
||||||
2026-06-11 09:40:55 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
|
||||||
2026-06-11 09:40:55 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
|
||||||
2026-06-11 09:40:55 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/api_key/list
|
|
||||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - close.started
|
|
||||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - close.complete
|
|
||||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
|
||||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57D2623C0>
|
|
||||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x000002B57D2642D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
|
||||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57D2626F0>
|
|
||||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
|
|
||||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
|
|
||||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:40:49 GMT'), (b'Content-Type', b'application/json'), (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'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
|
||||||
2026-06-11 09:40:56 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
|
||||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
|
|
||||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
|
|
||||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
|
|
||||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
|
|
||||||
2026-06-11 09:40:56 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
|
|
||||||
2026-06-11 09:40:56 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
|
||||||
2026-06-11 09:40:56 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
|
||||||
"total": 6,
|
|
||||||
"rows": [
|
|
||||||
{
|
|
||||||
"createBy": "",
|
|
||||||
"createTime": "2026-06-06 15:10:31",
|
|
||||||
"updateBy": "",
|
|
||||||
"updateTime": "2026-06-06 15:10:31",
|
|
||||||
"remark": null,
|
|
||||||
"id": "7",
|
|
||||||
"apiKey": "Lb8LgEJ7eBUU8QMifKUJvo9w6YLAotbKJ-w1DKU8ZrU",
|
|
||||||
"apiKeyName": "AWINBEXT",
|
|
||||||
"enterpriseId": "1937166012193443842",
|
|
||||||
"status": 0,
|
|
||||||
"expireTime": "2027-06-06T15:10:32.000+08:00"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"createBy": "",
|
|
||||||
"createTime": "2026-05-25 14:47:11",
|
|
||||||
"u...
|
|
||||||
2026-06-11 09:40:56 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
|
||||||
|
|||||||
@@ -1,15 +1,2 @@
|
|||||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:91] - [API错误] 登录过期,请重新登录
|
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:98] - [API错误] HTTP 400, 参数不合法
|
||||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.server - ERROR - [server.py:96] - 工具执行失败: list_datasources, 错误: 登录过期,请重新登录
|
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:107] - [API错误] HTTP 404, {"path": "/x"}
|
||||||
Traceback (most recent call last):
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\server.py", line 84, in handle_call_tool
|
|
||||||
result = await tool_instance.execute(arguments or {})
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\tools\datasources.py", line 30, in execute
|
|
||||||
return self.client.get("/api/datasource/connection/list", params=params)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 102, in get
|
|
||||||
return self._handle_response(response, url)
|
|
||||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
||||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 92, in _handle_response
|
|
||||||
raise Exception(error_msg)
|
|
||||||
Exception: 登录过期,请重新登录
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python3
|
||||||
"""
|
"""
|
||||||
lzwcai-mcp-agile-db MCP Server
|
lzwcai-mcp-agile-db MCP Server
|
||||||
数据库管理平台 MCP 工具服务,提供 33 个工具用于数据库管理、表操作、API 密钥管理等
|
数据库管理平台 MCP 工具服务,提供数据库管理、表/数据操作、技能、API 密钥、MQTT 同步、AI 训练等工具
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
@@ -104,19 +104,24 @@ async def handle_call_tool(
|
|||||||
|
|
||||||
async def run_server():
|
async def run_server():
|
||||||
"""运行 MCP Server (stdio 模式)"""
|
"""运行 MCP Server (stdio 模式)"""
|
||||||
async with stdio_server() as streams:
|
try:
|
||||||
await server.run(
|
async with stdio_server() as streams:
|
||||||
streams[0],
|
await server.run(
|
||||||
streams[1],
|
streams[0],
|
||||||
InitializationOptions(
|
streams[1],
|
||||||
server_name="lzwcai_mcp_agile_db",
|
InitializationOptions(
|
||||||
server_version="0.1.0",
|
server_name="lzwcai_mcp_agile_db",
|
||||||
capabilities=server.get_capabilities(
|
server_version="0.1.12",
|
||||||
notification_options=NotificationOptions(),
|
capabilities=server.get_capabilities(
|
||||||
experimental_capabilities={},
|
notification_options=NotificationOptions(),
|
||||||
|
experimental_capabilities={},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
)
|
||||||
)
|
finally:
|
||||||
|
# 释放全局 HTTP 客户端
|
||||||
|
if _api_client is not None:
|
||||||
|
await _api_client.close()
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
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.
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']}")
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
"""
|
"""
|
||||||
API 密钥管理工具 (工具 18-23)
|
API 密钥管理工具(含创建、状态切换、删除、权限查询/授予/撤销)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from ._base import register_tool, ToolDef
|
from ._base import register_tool, ToolDef
|
||||||
@@ -119,3 +119,34 @@ class GrantApiKeyPermissionsTool(ToolDef):
|
|||||||
|
|
||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
return await self.client.post("/datasource/api_key/permission/grant_batch", json_data=args)
|
return await self.client.post("/datasource/api_key/permission/grant_batch", json_data=args)
|
||||||
|
|
||||||
|
|
||||||
|
@register_tool("revoke_api_key_permissions")
|
||||||
|
class RevokeApiKeyPermissionsTool(ToolDef):
|
||||||
|
name = "revoke_api_key_permissions"
|
||||||
|
description = "撤销/删除 API 密钥已授予的权限(按权限记录 ID)"
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"permissionIds": {
|
||||||
|
"type": "array",
|
||||||
|
"items": {"type": "string"},
|
||||||
|
"description": (
|
||||||
|
"权限记录 ID 列表。"
|
||||||
|
"先从 get_api_key_permissions 获取,"
|
||||||
|
"取 connectionPermissions / databasePermissions / tablePermissions 中每项的 id 字段"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["permissionIds"],
|
||||||
|
}
|
||||||
|
|
||||||
|
async def execute(self, args: dict) -> dict:
|
||||||
|
args = dict(args)
|
||||||
|
permission_ids = args.pop("permissionIds", None) or []
|
||||||
|
# 过滤掉空字符串/None,防止拼接出类似 "1,,2" 的非法 ID
|
||||||
|
permission_ids = [pid for pid in permission_ids if pid is not None and str(pid).strip()]
|
||||||
|
if not permission_ids:
|
||||||
|
raise ValueError("permissionIds 不能为空")
|
||||||
|
ids = ",".join(str(pid).strip() for pid in permission_ids)
|
||||||
|
return await self.client.delete(f"/datasource/api_key/permission/{ids}")
|
||||||
|
|||||||
@@ -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"]})
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
数据导入工具 (工具 30-31)
|
数据导入工具 (工具 30-31)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import base64
|
|
||||||
import io
|
import io
|
||||||
|
import mimetypes
|
||||||
|
import os
|
||||||
|
from urllib.parse import urlparse, unquote
|
||||||
|
|
||||||
from ._base import register_tool, ToolDef
|
from ._base import register_tool, ToolDef
|
||||||
|
|
||||||
@@ -11,31 +13,75 @@ from ._base import register_tool, ToolDef
|
|||||||
@register_tool("preview_import_data")
|
@register_tool("preview_import_data")
|
||||||
class PreviewImportDataTool(ToolDef):
|
class PreviewImportDataTool(ToolDef):
|
||||||
name = "preview_import_data"
|
name = "preview_import_data"
|
||||||
description = "上传 Excel 文件,AI 智能识别并预览表结构/数据"
|
description = "从 URL 下载 Excel 文件,AI 智能识别并预览表结构/数据"
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"connectionId": {"type": "string", "description": "数据源 ID"},
|
"connectionId": {"type": "string", "description": "数据源 ID"},
|
||||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"},
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"},
|
||||||
"file_base64": {"type": "string", "description": "Excel 文件 base64 编码(.xlsx/.xls, <500KB)"},
|
"file_url": {"type": "string", "description": "Excel 文件下载地址(.xlsx/.xls, <500KB)"},
|
||||||
"file_name": {"type": "string", "description": "文件名(如 data.xlsx)"},
|
|
||||||
},
|
},
|
||||||
"required": ["connectionId", "file_base64"],
|
"required": ["connectionId", "file_url"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
args = dict(args)
|
args = dict(args)
|
||||||
connection_id = args.pop("connectionId")
|
connection_id = args.pop("connectionId")
|
||||||
target = args.pop("target", "test")
|
target = args.pop("target", "test")
|
||||||
file_base64 = args.pop("file_base64")
|
file_url = args.pop("file_url")
|
||||||
file_name = args.pop("file_name", "import.xlsx")
|
|
||||||
|
|
||||||
# 解码 base64 文件
|
# 0. 基础 URL 校验
|
||||||
file_content = base64.b64decode(file_base64)
|
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 = {
|
files = {
|
||||||
"file": (file_name, io.BytesIO(file_content), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
"file": (file_name, io.BytesIO(file_content), content_type),
|
||||||
}
|
}
|
||||||
|
|
||||||
return await self.client.upload(
|
return await self.client.upload(
|
||||||
@@ -44,29 +90,109 @@ class PreviewImportDataTool(ToolDef):
|
|||||||
params={"target": target},
|
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")
|
@register_tool("confirm_import_data")
|
||||||
class ConfirmImportDataTool(ToolDef):
|
class ConfirmImportDataTool(ToolDef):
|
||||||
name = "confirm_import_data"
|
name = "confirm_import_data"
|
||||||
description = "确认导入 AI 识别后的数据"
|
description = (
|
||||||
|
"确认导入 AI 识别后的数据(建表+插数据)。"
|
||||||
|
"传入 preview_import_data 返回的 data 原文 + databaseName 即可,"
|
||||||
|
"工具内部会自动组装成后端要求的 {tableStructure(含databaseName), allData} 结构"
|
||||||
|
)
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"connectionId": {"type": "string", "description": "数据源 ID"},
|
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||||
"target": {"type": "string", "enum": ["prod", "test"], "description": "环境"},
|
"databaseName": {"type": "string", "description": "落库的数据库名(导入目标库)"},
|
||||||
"data": {"type": "object", "description": "导入数据(含 tableStructure + allData)"},
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"},
|
||||||
|
"data": {"type": "object", "description": "preview_import_data 返回的 data(含 tableStructure/allData),或已组装好的最终结构"},
|
||||||
},
|
},
|
||||||
"required": ["connectionId", "data"],
|
"required": ["connectionId", "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
|
||||||
|
if isinstance(ts, dict):
|
||||||
|
if "columns" in ts:
|
||||||
|
# 已是单表对象(调用方自行组装过)
|
||||||
|
single_table = dict(ts)
|
||||||
|
else:
|
||||||
|
# preview 包装:tableStructure.data.tables[0]
|
||||||
|
inner = ts.get("data") if isinstance(ts.get("data"), dict) else {}
|
||||||
|
tables = inner.get("tables") if isinstance(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
|
||||||
|
|
||||||
|
all_data = data.get("allData")
|
||||||
|
if all_data is None:
|
||||||
|
all_data = data.get("data") or []
|
||||||
|
return {"tableStructure": single_table, "allData": all_data}
|
||||||
|
|
||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
args = dict(args)
|
args = dict(args)
|
||||||
connection_id = args.pop("connectionId")
|
connection_id = args.pop("connectionId")
|
||||||
target = args.pop("target", "test")
|
target = args.pop("target", "test")
|
||||||
|
database_name = args.pop("databaseName", None)
|
||||||
data = args.pop("data")
|
data = args.pop("data")
|
||||||
|
|
||||||
|
body = self._build_body(data, database_name)
|
||||||
return await self.client.post(
|
return await self.client.post(
|
||||||
f"/datasource/connection/{connection_id}/import_document/confirm",
|
f"/datasource/connection/{connection_id}/import_document/confirm",
|
||||||
json_data=data,
|
json_data=body,
|
||||||
params={"target": target},
|
params={"target": target},
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -45,6 +45,28 @@ class ListTablesTool(ToolDef):
|
|||||||
return await self.client.get("/datasource/table/list", params=params)
|
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")
|
@register_tool("get_table_detail")
|
||||||
class GetTableDetailTool(ToolDef):
|
class GetTableDetailTool(ToolDef):
|
||||||
name = "get_table_detail"
|
name = "get_table_detail"
|
||||||
@@ -117,8 +139,8 @@ class AlterTableTool(ToolDef):
|
|||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"operation": {"type": "string", "enum": ["ADD_COLUMN", "DROP_COLUMN", "RENAME_COLUMN", "ALTER_COLUMN_TYPE", "SET_NOT_NULL", "DROP_NOT_NULL", "SET_DEFAULT", "DROP_DEFAULT"], "description": "变更类型"},
|
"operation": {"type": "string", "enum": ["ADD_COLUMN", "MODIFY_COLUMN", "DROP_COLUMN"], "description": "变更类型:新增/修改/删除字段(后端实测仅支持这三种)"},
|
||||||
"column": {"type": "object", "description": "列定义(根据 operation 不同包含不同字段)"},
|
"column": {"type": "object", "description": "列定义。ADD/MODIFY 需 columnName+columnType(+columnLength/columnComment 等);DROP 仅需 columnName"},
|
||||||
},
|
},
|
||||||
"required": ["operation", "column"],
|
"required": ["operation", "column"],
|
||||||
},
|
},
|
||||||
@@ -137,7 +159,7 @@ class AlterTableTool(ToolDef):
|
|||||||
@register_tool("generate_table_by_description")
|
@register_tool("generate_table_by_description")
|
||||||
class GenerateTableByDescriptionTool(ToolDef):
|
class GenerateTableByDescriptionTool(ToolDef):
|
||||||
name = "generate_table_by_description"
|
name = "generate_table_by_description"
|
||||||
description = "通过自然语言描述让 AI 生成表结构(异步任务)"
|
description = "通过自然语言描述让 AI 生成表结构(异步任务,返回 taskId)。需配合 get_ai_training_detail 轮询 taskId 获取生成结果"
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@@ -149,3 +171,116 @@ class GenerateTableByDescriptionTool(ToolDef):
|
|||||||
|
|
||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
return await self.client.post("/datasource/connection/generate_table", json_data=args)
|
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)
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ class CreateDatasourceTool(ToolDef):
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"datasourceName": {"type": "string", "description": "数据源名称(3-20字)"},
|
"datasourceName": {"type": "string", "description": "数据源名称(3-20字)"},
|
||||||
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng"], "description": "数据库类型"},
|
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
|
||||||
"host": {"type": "string", "description": "数据库地址"},
|
"host": {"type": "string", "description": "数据库地址"},
|
||||||
"port": {"type": "integer", "description": "端口号"},
|
"port": {"type": "integer", "description": "端口号"},
|
||||||
"databaseName": {"type": "string", "description": "要连接的数据库名"},
|
"databaseName": {"type": "string", "description": "要连接的数据库名"},
|
||||||
@@ -88,7 +88,7 @@ class CreateDatasourceTool(ToolDef):
|
|||||||
args = dict(args)
|
args = dict(args)
|
||||||
test_first = args.pop("test_first", True)
|
test_first = args.pop("test_first", True)
|
||||||
|
|
||||||
# 如果需要先测试连接
|
# 如果需要先测试连接(测试失败时 client 会直接抛异常,由上层统一捕获)
|
||||||
if test_first:
|
if test_first:
|
||||||
test_data = {
|
test_data = {
|
||||||
"datasourceName": args.get("datasourceName"),
|
"datasourceName": args.get("datasourceName"),
|
||||||
@@ -100,9 +100,7 @@ class CreateDatasourceTool(ToolDef):
|
|||||||
"password": args.get("password"),
|
"password": args.get("password"),
|
||||||
"connectionType": args.get("connectionType", "user_password"),
|
"connectionType": args.get("connectionType", "user_password"),
|
||||||
}
|
}
|
||||||
test_result = await self.client.post("/datasource/connection/test", json_data=test_data)
|
await self.client.post("/datasource/connection/test", json_data=test_data)
|
||||||
if test_result.get("code") != 200:
|
|
||||||
return {"success": False, "error": f"连接测试失败: {test_result.get('msg', '未知错误')}"}
|
|
||||||
|
|
||||||
# 创建数据源
|
# 创建数据源
|
||||||
return await self.client.post("/datasource/connection", json_data=args)
|
return await self.client.post("/datasource/connection", json_data=args)
|
||||||
@@ -117,11 +115,13 @@ class UpdateDatasourceTool(ToolDef):
|
|||||||
"properties": {
|
"properties": {
|
||||||
"id": {"type": "string", "description": "数据源 ID"},
|
"id": {"type": "string", "description": "数据源 ID"},
|
||||||
"datasourceName": {"type": "string", "description": "更新名称"},
|
"datasourceName": {"type": "string", "description": "更新名称"},
|
||||||
|
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
|
||||||
"host": {"type": "string", "description": "更新地址"},
|
"host": {"type": "string", "description": "更新地址"},
|
||||||
"port": {"type": "integer", "description": "更新端口"},
|
"port": {"type": "integer", "description": "更新端口"},
|
||||||
"databaseName": {"type": "string", "description": "更新数据库名"},
|
"databaseName": {"type": "string", "description": "更新数据库名"},
|
||||||
"username": {"type": "string", "description": "更新用户名"},
|
"username": {"type": "string", "description": "更新用户名"},
|
||||||
"password": {"type": "string", "description": "新密码(不传则不变)"},
|
"password": {"type": "string", "description": "新密码(不传则不变)"},
|
||||||
|
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型"},
|
||||||
"remark": {"type": "string", "description": "更新描述"},
|
"remark": {"type": "string", "description": "更新描述"},
|
||||||
},
|
},
|
||||||
"required": ["id"],
|
"required": ["id"],
|
||||||
@@ -171,3 +171,79 @@ class DeleteDatasourceTool(ToolDef):
|
|||||||
|
|
||||||
# 删除数据源
|
# 删除数据源
|
||||||
return await self.client.delete(f"/datasource/connection/{ds_id}")
|
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")
|
||||||
@@ -119,14 +119,17 @@ class DeleteSkillToolTool(ToolDef):
|
|||||||
@register_tool("update_skill_config")
|
@register_tool("update_skill_config")
|
||||||
class UpdateSkillConfigTool(ToolDef):
|
class UpdateSkillConfigTool(ToolDef):
|
||||||
name = "update_skill_config"
|
name = "update_skill_config"
|
||||||
description = "更新技能配置(如 MCP Server 配置模板)"
|
description = "更新技能(名称/描述/MCP 配置模板等)。对应后端 skill/updateOrGet"
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||||
"configTemplate": {"type": "string", "description": "配置模板 JSON 字符串"},
|
"skillId": {"type": "string", "description": "技能 ID(可选)"},
|
||||||
|
"name": {"type": "string", "description": "技能名称(可选)"},
|
||||||
|
"description": {"type": "string", "description": "技能描述(可选)"},
|
||||||
|
"configTemplate": {"type": "string", "description": "配置模板 JSON 字符串(可选)"},
|
||||||
},
|
},
|
||||||
"required": ["datasourceId", "configTemplate"],
|
"required": ["datasourceId"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
@@ -135,3 +138,40 @@ class UpdateSkillConfigTool(ToolDef):
|
|||||||
if "configTemplate" in args and isinstance(args["configTemplate"], dict):
|
if "configTemplate" in args and isinstance(args["configTemplate"], dict):
|
||||||
args["configTemplate"] = json.dumps(args["configTemplate"])
|
args["configTemplate"] = json.dumps(args["configTemplate"])
|
||||||
return await self.client.post("/datasource/skill/updateOrGet", json_data=args)
|
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 语义)"
|
||||||
|
# 真实工具实体字段(已真机探测确认):主键 id、展示名 uniqueName、描述 description、
|
||||||
|
# SQL 模板 sqlTemplate、结果类型 resultType。后端不认 skillToolId/businessDescription/businessScenario。
|
||||||
|
input_schema = {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"id": {"type": "string", "description": "技能工具 ID(get_skill_tools 返回的 id)"},
|
||||||
|
"uniqueName": {"type": "string", "description": "工具展示名(可选)"},
|
||||||
|
"description": {"type": "string", "description": "工具描述(可选)"},
|
||||||
|
"sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"},
|
||||||
|
"resultType": {"type": "string", "enum": ["single", "list"], "description": "结果类型(可选)"},
|
||||||
|
},
|
||||||
|
"required": ["id"],
|
||||||
|
}
|
||||||
|
|
||||||
|
# 旧参数名 -> 真实字段名(向后兼容此前错误实现的调用方)
|
||||||
|
_LEGACY_MAP = {
|
||||||
|
"skillToolId": "id",
|
||||||
|
"name": "uniqueName",
|
||||||
|
"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)
|
||||||
|
# businessScenario 后端实体无此字段,丢弃避免干扰
|
||||||
|
args.pop("businessScenario", None)
|
||||||
|
return await self.client.post("/datasource/skill/tskilltool/updateOrGet", json_data=args)
|
||||||
|
|||||||
@@ -35,4 +35,11 @@ class ExecuteSqlTool(ToolDef):
|
|||||||
body["businessName"] = args["businessName"]
|
body["businessName"] = args["businessName"]
|
||||||
if "params" in args:
|
if "params" in args:
|
||||||
body["parameters"] = args["params"]
|
body["parameters"] = args["params"]
|
||||||
return await self.client.post("/datasource/sqlExecutionLog/testSqlWithSchema", json_data=body)
|
# 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,
|
||||||
|
)
|
||||||
|
|||||||
@@ -8,22 +8,24 @@ from ._base import register_tool, ToolDef
|
|||||||
@register_tool("toggle_table_subscription")
|
@register_tool("toggle_table_subscription")
|
||||||
class ToggleTableSubscriptionTool(ToolDef):
|
class ToggleTableSubscriptionTool(ToolDef):
|
||||||
name = "toggle_table_subscription"
|
name = "toggle_table_subscription"
|
||||||
description = "切换表的订阅状态"
|
description = "切换表的订阅状态(订阅依赖该表已配置 MQTT 字段关联,否则后端会报操作失败)"
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"tableId": {"type": "string", "description": "表 ID"},
|
"configId": {"type": "string", "description": "库/配置 ID(即 list_databases 返回的 config id)"},
|
||||||
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
"tableName": {"type": "string", "description": "表名(注意:后端按表名而非 tableId 识别)"},
|
||||||
"subscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"},
|
"subscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"},
|
||||||
},
|
},
|
||||||
"required": ["tableId", "datasourceId", "subscribe"],
|
"required": ["configId", "tableName", "subscribe"],
|
||||||
}
|
}
|
||||||
|
|
||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
# 映射参数名为后端 API 期望的格式
|
# 后端真实字段(已真机探测确认):configId + tableName + isSubscribe(bool)
|
||||||
|
# 兼容旧参数名 datasourceId->configId
|
||||||
|
config_id = args.get("configId") or args.get("datasourceId")
|
||||||
body = {
|
body = {
|
||||||
"tableId": args["tableId"],
|
"configId": config_id,
|
||||||
"datasourceId": args["datasourceId"],
|
"tableName": args.get("tableName"),
|
||||||
"subscribe": args["subscribe"],
|
"isSubscribe": args.get("subscribe"),
|
||||||
}
|
}
|
||||||
return await self.client.post("/datasource/subscription/toggle", json_data=body)
|
return await self.client.post("/datasource/subscription/toggle", json_data=body)
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ from ._base import register_tool, ToolDef
|
|||||||
@register_tool("query_table_data")
|
@register_tool("query_table_data")
|
||||||
class QueryTableDataTool(ToolDef):
|
class QueryTableDataTool(ToolDef):
|
||||||
name = "query_table_data"
|
name = "query_table_data"
|
||||||
description = "查询内置表数据(分页)"
|
description = "查询内置表数据(分页)。target 默认 test(调试环境);查询线上数据须显式传 target=prod"
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"tableId": {"type": "string", "description": "表 ID"},
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test(调试);线上数据传 prod"},
|
||||||
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||||
"pageSize": {"type": "integer", "default": 10, "description": "每页数量"},
|
"pageSize": {"type": "integer", "default": 10, "description": "每页数量"},
|
||||||
},
|
},
|
||||||
@@ -25,6 +25,7 @@ class QueryTableDataTool(ToolDef):
|
|||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
args = dict(args)
|
args = dict(args)
|
||||||
table_id = args.pop("tableId")
|
table_id = args.pop("tableId")
|
||||||
|
args.setdefault("target", "test")
|
||||||
params = {k: v for k, v in args.items() if v is not None}
|
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)
|
return await self.client.get(f"/datasource/connection/builtin/table/{table_id}", params=params)
|
||||||
|
|
||||||
@@ -32,12 +33,12 @@ class QueryTableDataTool(ToolDef):
|
|||||||
@register_tool("insert_table_row")
|
@register_tool("insert_table_row")
|
||||||
class InsertTableRowTool(ToolDef):
|
class InsertTableRowTool(ToolDef):
|
||||||
name = "insert_table_row"
|
name = "insert_table_row"
|
||||||
description = "向内置表插入一行数据"
|
description = "向内置表插入一行数据。⚠️ 写操作 target 默认 test(调试环境);写入线上库须显式传 target=prod"
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"tableId": {"type": "string", "description": "表 ID"},
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test(调试);写入线上库必须显式传 prod"},
|
||||||
"data": {"type": "object", "description": "行数据(键值对,键为字段名)"},
|
"data": {"type": "object", "description": "行数据(键值对,键为字段名)"},
|
||||||
},
|
},
|
||||||
"required": ["tableId", "data"],
|
"required": ["tableId", "data"],
|
||||||
@@ -46,21 +47,22 @@ class InsertTableRowTool(ToolDef):
|
|||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
args = dict(args)
|
args = dict(args)
|
||||||
table_id = args.pop("tableId")
|
table_id = args.pop("tableId")
|
||||||
target = args.pop("target", "prod")
|
target = args.pop("target", "test")
|
||||||
data = args.pop("data", {})
|
data = args.pop("data", {})
|
||||||
params = {"target": target} if target else {}
|
params = {"target": target} if target else {}
|
||||||
return await self.client.post(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=data, params=params)
|
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")
|
@register_tool("update_table_row")
|
||||||
class UpdateTableRowTool(ToolDef):
|
class UpdateTableRowTool(ToolDef):
|
||||||
name = "update_table_row"
|
name = "update_table_row"
|
||||||
description = "更新内置表的指定行"
|
description = "更新内置表的指定行。⚠️ 写操作 target 默认 test(调试环境);修改线上库须显式传 target=prod"
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"tableId": {"type": "string", "description": "表 ID"},
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test(调试);修改线上库必须显式传 prod"},
|
||||||
"primaryKey": {"type": "object", "description": "主键值(如 {\"id\": 1})"},
|
"primaryKey": {"type": "object", "description": "主键值(如 {\"id\": 1})"},
|
||||||
"data": {"type": "object", "description": "要更新的字段值"},
|
"data": {"type": "object", "description": "要更新的字段值"},
|
||||||
},
|
},
|
||||||
@@ -70,7 +72,7 @@ class UpdateTableRowTool(ToolDef):
|
|||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
args = dict(args)
|
args = dict(args)
|
||||||
table_id = args.pop("tableId")
|
table_id = args.pop("tableId")
|
||||||
target = args.pop("target", "prod")
|
target = args.pop("target", "test")
|
||||||
primary_key = args.pop("primaryKey")
|
primary_key = args.pop("primaryKey")
|
||||||
data = args.pop("data", {})
|
data = args.pop("data", {})
|
||||||
params = {"target": target} if target else {}
|
params = {"target": target} if target else {}
|
||||||
@@ -81,12 +83,12 @@ class UpdateTableRowTool(ToolDef):
|
|||||||
@register_tool("delete_table_rows")
|
@register_tool("delete_table_rows")
|
||||||
class DeleteTableRowsTool(ToolDef):
|
class DeleteTableRowsTool(ToolDef):
|
||||||
name = "delete_table_rows"
|
name = "delete_table_rows"
|
||||||
description = "删除内置表的指定行(根据主键批量删除)"
|
description = "删除内置表的指定行(根据主键批量删除)。⚠️ 写操作 target 默认 test(调试环境);删除线上库数据须显式传 target=prod"
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"tableId": {"type": "string", "description": "表 ID"},
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test(调试);删除线上库数据必须显式传 prod"},
|
||||||
"primaryKeys": {
|
"primaryKeys": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "主键数组(如 [{\"id\": 1}, {\"id\": 2}])",
|
"description": "主键数组(如 [{\"id\": 1}, {\"id\": 2}])",
|
||||||
@@ -99,7 +101,7 @@ class DeleteTableRowsTool(ToolDef):
|
|||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
args = dict(args)
|
args = dict(args)
|
||||||
table_id = args.pop("tableId")
|
table_id = args.pop("tableId")
|
||||||
target = args.pop("target", "prod")
|
target = args.pop("target", "test")
|
||||||
primary_keys = args.pop("primaryKeys")
|
primary_keys = args.pop("primaryKeys")
|
||||||
params = {"target": target} if target else {}
|
params = {"target": target} if target else {}
|
||||||
body = {"primaryKeys": primary_keys}
|
body = {"primaryKeys": primary_keys}
|
||||||
@@ -109,12 +111,12 @@ class DeleteTableRowsTool(ToolDef):
|
|||||||
@register_tool("export_table_excel")
|
@register_tool("export_table_excel")
|
||||||
class ExportTableExcelTool(ToolDef):
|
class ExportTableExcelTool(ToolDef):
|
||||||
name = "export_table_excel"
|
name = "export_table_excel"
|
||||||
description = "导出表数据为 Excel 文件(返回 base64 编码)"
|
description = "导出表数据为 Excel 文件(返回 base64 编码)。target 默认 test(调试环境);导出线上数据须显式传 target=prod"
|
||||||
input_schema = {
|
input_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"tableId": {"type": "string", "description": "表 ID"},
|
"tableId": {"type": "string", "description": "表 ID"},
|
||||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test(调试);线上数据传 prod"},
|
||||||
},
|
},
|
||||||
"required": ["tableId"],
|
"required": ["tableId"],
|
||||||
}
|
}
|
||||||
@@ -122,7 +124,7 @@ class ExportTableExcelTool(ToolDef):
|
|||||||
async def execute(self, args: dict) -> dict:
|
async def execute(self, args: dict) -> dict:
|
||||||
args = dict(args)
|
args = dict(args)
|
||||||
table_id = args.pop("tableId")
|
table_id = args.pop("tableId")
|
||||||
target = args.pop("target", "prod")
|
target = args.pop("target", "test")
|
||||||
params = {"target": target} if target else {}
|
params = {"target": target} if target else {}
|
||||||
result = await self.client.get(f"/datasource/connection/builtin/table/{table_id}/export/excel", params=params)
|
result = await self.client.get(f"/datasource/connection/builtin/table/{table_id}/export/excel", params=params)
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -64,14 +64,14 @@ class AgileDBAPIClient:
|
|||||||
return headers
|
return headers
|
||||||
|
|
||||||
def _build_url(self, path: str) -> str:
|
def _build_url(self, path: str) -> str:
|
||||||
"""构建完整 URL,自动去掉路径中多余的 /api 前缀"""
|
"""构建完整 URL
|
||||||
|
|
||||||
|
约定:base_url 已包含完整地址(如含 /api 前缀则配在环境变量里),
|
||||||
|
各工具传入的 path 不带 /api 前缀,此处直接拼接。
|
||||||
|
"""
|
||||||
if path.startswith('http://') or path.startswith('https://'):
|
if path.startswith('http://') or path.startswith('https://'):
|
||||||
return path
|
return path
|
||||||
# 去掉 /api 前缀,因为 base_url 已经包含完整地址
|
return f"{self.base_url}{path}"
|
||||||
clean_path = path
|
|
||||||
if clean_path.startswith('/api'):
|
|
||||||
clean_path = clean_path[4:]
|
|
||||||
return f"{self.base_url}{clean_path}"
|
|
||||||
|
|
||||||
def _handle_response(self, response: httpx.Response, url: str) -> Dict[str, Any]:
|
def _handle_response(self, response: httpx.Response, url: str) -> Dict[str, Any]:
|
||||||
"""统一处理 API 响应"""
|
"""统一处理 API 响应"""
|
||||||
@@ -80,20 +80,32 @@ class AgileDBAPIClient:
|
|||||||
if response.status_code == 204:
|
if response.status_code == 204:
|
||||||
return {"success": True, "data": None}
|
return {"success": True, "data": None}
|
||||||
|
|
||||||
response.raise_for_status()
|
# 先尝试解析 body,再判断状态码。
|
||||||
|
# 平台会用非 2xx 状态码 + body 里的 {code, msg} 返回业务错误,
|
||||||
|
# 若先 raise_for_status() 会在解析 body 前抛异常,导致真正的 msg 全部丢失。
|
||||||
try:
|
try:
|
||||||
data = response.json()
|
data = response.json()
|
||||||
except json.JSONDecodeError:
|
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||||
# 非 JSON 响应(如文件下载)
|
# 非 JSON 响应(如 Excel/文件下载,二进制内容 .json() 会抛 UnicodeDecodeError)
|
||||||
|
response.raise_for_status()
|
||||||
return {"success": True, "data": response.content, "raw": True}
|
return {"success": True, "data": response.content, "raw": True}
|
||||||
|
|
||||||
# 检查平台 API 的 {code, msg} 格式
|
# 检查平台 API 的 {code, msg} 格式
|
||||||
|
# 平台约定:code 为 200 或 0 均表示成功(见数据库模块文档 §1.1)
|
||||||
if isinstance(data, dict) and 'code' in data:
|
if isinstance(data, dict) and 'code' in data:
|
||||||
if data['code'] != 200:
|
if data['code'] not in (0, 200):
|
||||||
error_msg = data.get('msg', '未知错误')
|
error_msg = data.get('msg', '未知错误')
|
||||||
logger.error(f"[API错误] {error_msg}")
|
logger.error(f"[API错误] HTTP {response.status_code}, {error_msg}")
|
||||||
raise Exception(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
|
return data
|
||||||
|
|
||||||
@@ -147,9 +159,13 @@ class AgileDBAPIClient:
|
|||||||
try:
|
try:
|
||||||
logger.info(f"[API请求] DELETE {url}")
|
logger.info(f"[API请求] DELETE {url}")
|
||||||
headers = self._get_headers()
|
headers = self._get_headers()
|
||||||
|
# 注意:httpx 的 client.delete() 便捷方法不接受 json 参数(仅 post/put/patch 有)。
|
||||||
|
# 需要带 body 的 DELETE 必须走通用 request(),否则会抛 TypeError。
|
||||||
if json_data is not None:
|
if json_data is not None:
|
||||||
headers['Content-Type'] = 'application/json'
|
headers['Content-Type'] = 'application/json'
|
||||||
response = await self.client.delete(url, headers=headers, params=params, json=json_data)
|
response = await self.client.request("DELETE", url, headers=headers, params=params, json=json_data)
|
||||||
|
else:
|
||||||
|
response = await self.client.delete(url, headers=headers, params=params)
|
||||||
return self._handle_response(response, url)
|
return self._handle_response(response, url)
|
||||||
except httpx.TimeoutException:
|
except httpx.TimeoutException:
|
||||||
raise Exception(f"API 请求超时: {url}")
|
raise Exception(f"API 请求超时: {url}")
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def get_env_config() -> dict:
|
|||||||
dict: 包含所有配置的字典
|
dict: 包含所有配置的字典
|
||||||
"""
|
"""
|
||||||
return {
|
return {
|
||||||
"api_key": get_api_key(""),
|
"api_key": os.environ.get("API_KEY", ""),
|
||||||
"base_url": get_base_url(),
|
"base_url": get_base_url(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
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 |
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| datasourceId | string | 是 | 数据源 ID |
|
| connectionId | string | 是 | 数据源连接 ID(路径参数) |
|
||||||
| databaseName | string | 是 | 目标数据库名 |
|
| databaseName | string | 是 | 目标数据库名 |
|
||||||
| tableName | string | 是 | 表名(小写字母+数字+下划线) |
|
| tableName | string | 是 | 表名(小写字母+数字+下划线) |
|
||||||
| tableComment | string | 否 | 表注释 |
|
| tableComment | string | 否 | 表注释 |
|
||||||
@@ -198,33 +198,29 @@
|
|||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| tableId | string | 是 | 表 ID |
|
| connectionId | string | 是 | 数据源连接 ID(路径参数) |
|
||||||
| columns | array | 是 | 字段变更数组 |
|
| databaseName | string | 是 | 数据库名 |
|
||||||
| columns[].operation | string | 是 | ADD_COLUMN / MODIFY_COLUMN / DROP_COLUMN |
|
| tableName | string | 是 | 表名 |
|
||||||
| columns[].columnName | string | 是 | 字段名 |
|
| operations | array | 是 | 表结构变更操作数组 |
|
||||||
| columns[].columnType | string | 否 | 字段类型(ADD/MODIFY) |
|
| operations[].operation | string | 是 | ADD_COLUMN / DROP_COLUMN / RENAME_COLUMN / ALTER_COLUMN_TYPE / SET_NOT_NULL / DROP_NOT_NULL / SET_DEFAULT / DROP_DEFAULT |
|
||||||
| columns[].columnLength | int | 否 | 字段长度 |
|
| operations[].column | object | 是 | 列定义(根据 operation 不同包含不同字段,如 columnName/columnType/newColumnName 等) |
|
||||||
| columns[].isPrimaryKey | bool | 否 | 是否主键 |
|
| tableComment | string | 否 | 新表注释 |
|
||||||
| columns[].isNullable | bool | 否 | 是否可空 |
|
|
||||||
| columns[].columnComment | string | 否 | 字段注释 |
|
|
||||||
| columns[].defaultValue | string | 否 | 默认值 |
|
|
||||||
| newTableName | string | 否 | 新表名(重命名) |
|
|
||||||
| newTableComment | string | 否 | 新表注释 |
|
|
||||||
|
|
||||||
**返回**:修改结果
|
**返回**:修改结果
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
#### 12. `generate_table_by_description`
|
#### 12. `generate_table_by_description`
|
||||||
- **用途**:通过自然语言描述让 AI 生成表结构
|
- **用途**:通过自然语言描述让 AI 生成表结构(异步任务)
|
||||||
- **对应前端**:CreateBuiltinDataSource.vue AI 生成表结构
|
- **对应前端**:CreateBuiltinDataSource.vue AI 生成表结构
|
||||||
- **对应 API**:`postGenerateTable(data)` ✅ 已实现 — `POST /api/datasource/connection/generate_table`
|
- **对应 API**:`postGenerateTable(data)` ✅ 已实现 — `POST /api/datasource/connection/generate_table`
|
||||||
|
|
||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| description | string | 是 | 业务场景描述(至少6个字符) |
|
| requirement | string | 是 | 业务需求描述 |
|
||||||
|
| databaseId | int | 否 | 关联的数据库 ID |
|
||||||
|
|
||||||
**返回**:AI 生成的表结构(表名、表注释、字段列表含类型/主键/注释等)
|
**返回**:异步任务信息(taskId、status),需轮询任务状态获取最终生成的表结构
|
||||||
|
|
||||||
> **场景示例**:用户说"我需要一个商城系统,管理商品、分类和用户评价",AI 返回完整的表结构设计。
|
> **场景示例**:用户说"我需要一个商城系统,管理商品、分类和用户评价",AI 返回完整的表结构设计。
|
||||||
|
|
||||||
@@ -389,6 +385,18 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
#### 23.5 `revoke_api_key_permissions`
|
||||||
|
- **用途**:撤销/删除 API 密钥已授予的权限(按权限记录 ID)
|
||||||
|
- **对应 API**:按现有 delete 风格推测为 `DELETE /api/datasource/api_key/permission/{ids}`,需后端真机验证
|
||||||
|
|
||||||
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|
|------|------|------|------|
|
||||||
|
| permissionIds | array[string] | 是 | 权限记录 ID 列表。先从 `get_api_key_permissions` 获取,取 `connectionPermissions` / `databasePermissions` / `tablePermissions` 中每项的 `id` |
|
||||||
|
|
||||||
|
**返回**:撤销结果
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
### 🤖 技能与工具管理 (内置数据源 AI 能力)
|
### 🤖 技能与工具管理 (内置数据源 AI 能力)
|
||||||
|
|
||||||
#### 24. `get_skill_by_datasource`
|
#### 24. `get_skill_by_datasource`
|
||||||
@@ -437,12 +445,14 @@
|
|||||||
| 参数 | 类型 | 必填 | 说明 |
|
| 参数 | 类型 | 必填 | 说明 |
|
||||||
|------|------|------|------|
|
|------|------|------|------|
|
||||||
| skillId | string | 是 | 技能 ID |
|
| skillId | string | 是 | 技能 ID |
|
||||||
| name | string | 是 | 工具名称 |
|
| tableIds | array | 否 | 关联的表 ID 数组 |
|
||||||
| businessDescription | string | 是 | 业务描述 |
|
| suggestions | array | 是 | SQL 工具建议数组(支持批量) |
|
||||||
| sqlTemplate | string | 是 | SQL 模板(支持 #{param} 参数占位) |
|
| suggestions[].name | string | 是 | 工具名称 |
|
||||||
| sqlParams | string | 否 | 参数 JSON Schema(默认空对象) |
|
| suggestions[].businessDescription | string | 是 | 业务描述 |
|
||||||
| resultType | string | 否 | single/list,默认 list |
|
| suggestions[].sqlTemplate | string | 是 | SQL 模板(支持 #{param} 参数占位) |
|
||||||
| businessScenario | string | 否 | 业务场景描述 |
|
| suggestions[].sqlParams | string/object | 否 | 参数 JSON Schema(对象会自动序列化为字符串) |
|
||||||
|
| suggestions[].resultType | string | 否 | single/list,默认 list |
|
||||||
|
| suggestions[].businessScenario | string | 否 | 业务场景描述 |
|
||||||
|
|
||||||
**返回**:工具创建结果
|
**返回**:工具创建结果
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,6 @@ from lzwcai_mcp_agile_db.server import main
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
os.environ["API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjJiMDY0YzMzLTBiZWYtNDU0NC04NWY1LTRmNTFiOGMxMmI5NSJ9.Uv599TvlQvlTlwrnZGo3Tl2eLAvM0ldE9vpMI5jHxbTf4_tVSRA60rUNIV4LBiw6pt1r_xIi7aFcTRE2PeN5sg"
|
os.environ["API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6ImNlMDAwYjA4LWU0YTYtNGM2MS1hNzJiLWI3NTlmNmY1N2Q4NCJ9.jiNmGQZfL4-nSIFrLuaCt7mT5zj0FOojAVkLeHwPOroI5jBxodrCe1PSwGO1OHq5Ztb0tLEVZw2FFVj0OlTceQ"
|
||||||
os.environ["backendBaseUrl"] = "https://dempdemo.lzwcai.com"
|
os.environ["backendBaseUrl"] = "http://192.168.2.236:8088"
|
||||||
main()
|
main()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "lzwcai-mcp-agile-db"
|
name = "lzwcai-mcp-agile-db"
|
||||||
version = "0.1.3"
|
version = "0.1.7"
|
||||||
description = "MCP server for database management platform with 33 tools for datasource, table, data, API key, and skill management"
|
description = "MCP server for database management platform with 33 tools for datasource, table, data, API key, and skill management"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
requires-python = ">=3.10"
|
||||||
|
|||||||
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.5"
|
||||||
|
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/**",
|
||||||
|
]
|
||||||
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.
36
lzwcai_mcpskills_generate_reports/.gitignore
vendored
Normal file
36
lzwcai_mcpskills_generate_reports/.gitignore
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
# Python
|
||||||
|
__pycache__/
|
||||||
|
*.py[cod]
|
||||||
|
*$py.class
|
||||||
|
*.so
|
||||||
|
.Python
|
||||||
|
build/
|
||||||
|
develop-eggs/
|
||||||
|
dist/
|
||||||
|
downloads/
|
||||||
|
eggs/
|
||||||
|
.eggs/
|
||||||
|
lib/
|
||||||
|
lib64/
|
||||||
|
parts/
|
||||||
|
sdist/
|
||||||
|
var/
|
||||||
|
wheels/
|
||||||
|
*.egg-info/
|
||||||
|
.installed.cfg
|
||||||
|
*.egg
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
ENV/
|
||||||
|
.venv/
|
||||||
|
|
||||||
|
# IDE
|
||||||
|
.vscode/
|
||||||
|
.idea/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
|
||||||
|
# Project outputs
|
||||||
|
_out/
|
||||||
159
lzwcai_mcpskills_generate_reports/README.md
Normal file
159
lzwcai_mcpskills_generate_reports/README.md
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
# lzwcai-mcpskills-generate-reports
|
||||||
|
|
||||||
|
用户提供 **docx 模板 + JSON 数据**,本包负责渲染成 docx,并可选做样式迁移。
|
||||||
|
|
||||||
|
本包**不内置模板**,模板完全由调用方维护。
|
||||||
|
|
||||||
|
## 安装
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
cd lzwcai_mcpskills_generate_reports
|
||||||
|
pip install -e .
|
||||||
|
```
|
||||||
|
|
||||||
|
## Python API
|
||||||
|
|
||||||
|
### 渲染文档
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lzwcai_mcpskills_generate_reports import generate
|
||||||
|
|
||||||
|
out_path = generate(
|
||||||
|
data="data.json", # dict 或 JSON 文件路径
|
||||||
|
template="./模板.docx", # 用户自己的 docx 模板路径
|
||||||
|
out_path="_out/报价方案.docx",
|
||||||
|
style_ref="./用户样式.docx", # 可选:把用户模板的 theme/字体套过来
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 扫描模板占位符
|
||||||
|
|
||||||
|
```python
|
||||||
|
from lzwcai_mcpskills_generate_reports import scan_template
|
||||||
|
|
||||||
|
result = scan_template("./模板.docx")
|
||||||
|
print(result)
|
||||||
|
# {
|
||||||
|
# "placeholders": ["project_title", "contact_person", "equipments", ...],
|
||||||
|
# "blocks": [
|
||||||
|
# {"type": "for", "iterator": "eq", "variable": "equipments"},
|
||||||
|
# {"type": "if", "condition": "show_layout"},
|
||||||
|
# ...
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
```
|
||||||
|
|
||||||
|
## 命令行
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 渲染
|
||||||
|
generate-report generate --template ./模板.docx --data data.json --out _out/报价方案.docx
|
||||||
|
|
||||||
|
# 扫描占位符
|
||||||
|
generate-report scan --template ./模板.docx
|
||||||
|
|
||||||
|
# 样式迁移
|
||||||
|
generate-report generate --template ./模板.docx --data data.json --style-ref ./用户样式.docx --out _out/报价方案_定制.docx
|
||||||
|
```
|
||||||
|
|
||||||
|
## MCP Server
|
||||||
|
|
||||||
|
本包同时提供 MCP Server(stdio 模式),把渲染引擎暴露成 3 个 MCP 工具:
|
||||||
|
|
||||||
|
| 工具 | 说明 | 必填参数 |
|
||||||
|
|------|------|----------|
|
||||||
|
| `generate_report` | 模板 + 数据 → 渲染输出 docx,返回输出文件绝对路径 | `template`, `data`, `out`(可选 `style_ref`) |
|
||||||
|
| `scan_template` | 扫描模板占位符与 for/if 块结构 | `template` |
|
||||||
|
| `validate_report_data` | 校验数据契约(不渲染) | `data` |
|
||||||
|
|
||||||
|
其中 `data` 既可以传 JSON 对象,也可以传指向 JSON 文件的路径字符串。
|
||||||
|
|
||||||
|
### 启动
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
# 安装后用 console script 启动
|
||||||
|
lzwcai-mcpskills-generate-reports
|
||||||
|
|
||||||
|
# 或直接运行入口模块
|
||||||
|
python main.py
|
||||||
|
```
|
||||||
|
|
||||||
|
### MCP 客户端配置示例
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"mcpServers": {
|
||||||
|
"generate-reports": {
|
||||||
|
"command": "lzwcai-mcpskills-generate-reports"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> stdio 模式下 stdout 被 MCP 协议占用,所有日志写入 `logs/` 目录与 stderr。
|
||||||
|
|
||||||
|
## 数据契约(QuoteData)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"project_title": "大米罐装线",
|
||||||
|
"contact_person": "张经理",
|
||||||
|
"contact_phone": "138-0000-0000",
|
||||||
|
"requirements": ["要求1", "要求2"],
|
||||||
|
"layout_image": "",
|
||||||
|
"layout_title": "整线布局尺寸图",
|
||||||
|
"equipments": [
|
||||||
|
{
|
||||||
|
"index": "四",
|
||||||
|
"name": "自动理瓶机",
|
||||||
|
"images": [""],
|
||||||
|
"features": [{"title": "特点", "lines": ["说明"]}],
|
||||||
|
"params": [{"k": "材料", "v": "不锈钢"}]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quote_items": [
|
||||||
|
{"name": "设备名", "qty": "一套", "image": "", "desc": "说明", "price": "面议"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
图片字段约定:路径/URL → 真实图;`""` → 默认占位图;`None` → 不显示。
|
||||||
|
|
||||||
|
## 目录结构
|
||||||
|
|
||||||
|
```
|
||||||
|
lzwcai_mcpskills_generate_reports/
|
||||||
|
├── pyproject.toml
|
||||||
|
├── README.md
|
||||||
|
├── templates/ # 用户模板(示例,不在包内)
|
||||||
|
│ └── standard/
|
||||||
|
│ ├── template.docx
|
||||||
|
│ └── meta.json
|
||||||
|
├── samples/ # 示例数据(不在包内)
|
||||||
|
│ └── sample_data.json
|
||||||
|
└── lzwcai_mcpskills_generate_reports/ # Python 包
|
||||||
|
├── __init__.py # 公共 API 入口
|
||||||
|
├── cli.py # 命令行
|
||||||
|
├── pipeline.py # 总入口
|
||||||
|
├── schema.py # 数据契约 + 校验器
|
||||||
|
├── render_quote.py # 渲染引擎
|
||||||
|
├── style_transfer.py # 样式迁移
|
||||||
|
└── template_scanner.py # 模板占位符扫描
|
||||||
|
```
|
||||||
|
|
||||||
|
## 模板约定
|
||||||
|
|
||||||
|
- 使用 [Jinja2](https://jinja.palletsprojects.com/) 语法写占位符,如 `{{ project_title }}`、`{% for eq in equipments %}`。
|
||||||
|
- 模板文件旁的 `meta.json`(可选)声明图片字段宽度,例如:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"image_fields": {
|
||||||
|
"layout_image": {"width_mm": 160},
|
||||||
|
"equipments[].images[]": {"width_mm": 120},
|
||||||
|
"quote_items[].image": {"width_mm": 30}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- BUSINESS 逻辑(内置模板维护、模板市场等)交给包外的上层系统处理。
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
3.12
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
lzwcai-mcpskills-generate-reports
|
||||||
|
|
||||||
|
纯渲染引擎,不内置模板。
|
||||||
|
对外暴露的公共 API:
|
||||||
|
- generate: 数据 + 模板路径 -> docx(核心入口)
|
||||||
|
- scan_template: 模板路径 -> 占位符 JSON
|
||||||
|
- validate: 校验数据契约
|
||||||
|
- normalize: 归一化数据
|
||||||
|
- transplant_style: 将用户模板样式迁移到结果文档
|
||||||
|
"""
|
||||||
|
|
||||||
|
__version__ = "0.1.0"
|
||||||
|
|
||||||
|
from .pipeline import generate
|
||||||
|
from .template_scanner import scan_template
|
||||||
|
from .schema import validate, normalize, DEFAULTS
|
||||||
|
from .style_transfer import transplant_style
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"generate",
|
||||||
|
"scan_template",
|
||||||
|
"validate",
|
||||||
|
"normalize",
|
||||||
|
"transplant_style",
|
||||||
|
"DEFAULTS",
|
||||||
|
"__version__",
|
||||||
|
]
|
||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,122 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
cli.py:公共入口。
|
||||||
|
|
||||||
|
程序调用(推荐):
|
||||||
|
from lzwcai_mcpskills_generate_reports.cli import generate_report, scan_report
|
||||||
|
|
||||||
|
generate_report(
|
||||||
|
template="./模板.docx",
|
||||||
|
data={"project_name": "x", ...},
|
||||||
|
out="_out/a.docx",
|
||||||
|
)
|
||||||
|
scan_report(template="./模板.docx")
|
||||||
|
|
||||||
|
命令行:
|
||||||
|
generate-report generate --template ./模板.docx --data data.json --out _out/a.docx
|
||||||
|
generate-report scan --template ./模板.docx
|
||||||
|
"""
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except (AttributeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
from lzwcai_mcpskills_generate_reports import generate, scan_template
|
||||||
|
|
||||||
|
|
||||||
|
def _load_data(data):
|
||||||
|
"""把 data 归一化为 dict:支持 dict 或 JSON 文件路径字符串。"""
|
||||||
|
if isinstance(data, str):
|
||||||
|
if not os.path.isfile(data):
|
||||||
|
raise FileNotFoundError(f"数据文件不存在: {data}")
|
||||||
|
with open(data, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise TypeError(f"data 必须是 dict 或 JSON 文件路径字符串,实际类型: {type(data).__name__}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def generate_report(template, data, out, style_ref=None):
|
||||||
|
"""生成 docx 报告。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
template: 模板 docx 文件路径
|
||||||
|
data: dict 数据 或 JSON 文件路径字符串
|
||||||
|
out: 输出 docx 文件路径
|
||||||
|
style_ref: 用户样式参考 docx 路径(可选)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
dict: {"output": 输出文件绝对路径}
|
||||||
|
"""
|
||||||
|
data = _load_data(data)
|
||||||
|
out = generate(data=data, template=template, out_path=out, style_ref=style_ref)
|
||||||
|
return {"output": out}
|
||||||
|
|
||||||
|
|
||||||
|
def scan_report(template):
|
||||||
|
"""扫描模板占位符。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
template: 模板 docx 文件路径
|
||||||
|
|
||||||
|
返回:
|
||||||
|
dict: 占位符扫描结果
|
||||||
|
"""
|
||||||
|
return scan_template(template)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_arg_parser():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="docx 模板渲染与占位符扫描",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
示例:
|
||||||
|
generate-report generate --template ./模板.docx --data data.json --out _out/a.docx
|
||||||
|
generate-report generate --template ./模板.docx --data data.json --style-ref 用户样式.docx --out _out/b.docx
|
||||||
|
generate-report scan --template ./模板.docx
|
||||||
|
""",
|
||||||
|
)
|
||||||
|
sub = parser.add_subparsers(dest="command", required=True)
|
||||||
|
|
||||||
|
gen_parser = sub.add_parser("generate", help="渲染生成 docx")
|
||||||
|
gen_parser.add_argument("--template", required=True, help="模板 docx 文件路径")
|
||||||
|
gen_parser.add_argument("--data", required=True, help="数据 JSON 文件路径")
|
||||||
|
gen_parser.add_argument("--out", required=True, help="输出 docx 文件路径")
|
||||||
|
gen_parser.add_argument("--style-ref", default=None, help="用户上传的样式参考 docx(可选)")
|
||||||
|
|
||||||
|
scan_parser = sub.add_parser("scan", help="扫描模板占位符")
|
||||||
|
scan_parser.add_argument("--template", required=True, help="模板 docx 文件路径")
|
||||||
|
|
||||||
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""命令行入口(console_scripts 调用)。"""
|
||||||
|
parser = _build_arg_parser()
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
if args.command == "generate":
|
||||||
|
result = generate_report(
|
||||||
|
template=args.template,
|
||||||
|
data=args.data,
|
||||||
|
out=args.out,
|
||||||
|
style_ref=args.style_ref,
|
||||||
|
)
|
||||||
|
print(f"生成成功: {result['output']}")
|
||||||
|
elif args.command == "scan":
|
||||||
|
result = scan_report(template=args.template)
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
except Exception as e:
|
||||||
|
print(f"错误: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
2026-06-16 16:27:35 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs
|
||||||
|
2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports'
|
||||||
|
2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||||
|
2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||||
|
2026-06-16 16:27:35 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||||
|
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具
|
||||||
|
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=validate_report_data, arguments={"data": "samples\\sample_data.json"}
|
||||||
|
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: validate_report_data
|
||||||
|
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=scan_template, arguments={"template": "templates\\standard\\template.docx"}
|
||||||
|
2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: scan_template
|
||||||
|
2026-06-16 16:28:01 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs
|
||||||
|
2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports'
|
||||||
|
2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||||
|
2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||||
|
2026-06-16 16:28:01 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||||
|
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具
|
||||||
|
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=validate_report_data, arguments={"data": "samples\\sample_data.json"}
|
||||||
|
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: validate_report_data
|
||||||
|
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=scan_template, arguments={"template": "templates\\standard\\template.docx"}
|
||||||
|
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: scan_template
|
||||||
|
2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=generate_report, arguments={"template": "templates\\standard\\template.docx", "data": "samples\\sample_data.json", "out": "_out\\mcp_test.docx"}
|
||||||
|
2026-06-16 16:28:20 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: generate_report
|
||||||
|
2026-06-17 10:11:02 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs
|
||||||
|
2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports'
|
||||||
|
2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||||
|
2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||||
|
2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:215] - ==================================================
|
||||||
|
2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:216] - lzwcai-mcpskills-generate-reports MCP Server 启动
|
||||||
|
2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:217] - ==================================================
|
||||||
|
2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:218] - 开始运行 MCP Server (stdio 模式)
|
||||||
|
2026-06-17 10:11:02 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||||
|
2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||||
|
2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001E55CC2C2C0>
|
||||||
|
2026-06-17 10:11:03 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||||
|
2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||||
|
2026-06-17 10:11:03 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具
|
||||||
|
2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
pipeline.py:总入口 — 串起 数据校验 -> 渲染 -> (可选)样式迁移。
|
||||||
|
|
||||||
|
注意:本包不维护内置模板。调用方需要自己准备 .docx 模板文件,
|
||||||
|
并通过 template 参数传入路径。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
|
||||||
|
from .schema import validate, normalize
|
||||||
|
from .render_quote import render
|
||||||
|
from .style_transfer import transplant_style
|
||||||
|
|
||||||
|
|
||||||
|
def _load_data(data):
|
||||||
|
"""把 data 归一化为 dict:支持 dict 或 JSON 文件路径。"""
|
||||||
|
if isinstance(data, str):
|
||||||
|
if not os.path.isfile(data):
|
||||||
|
raise FileNotFoundError(f"数据文件不存在: {data}")
|
||||||
|
with open(data, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise TypeError(f"data 必须是 dict 或 JSON 文件路径,实际类型: {type(data).__name__}")
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _load_meta_for_template(template_path):
|
||||||
|
"""如果模板同目录下存在 meta.json,则读取;否则返回空 dict。"""
|
||||||
|
meta_file = os.path.join(os.path.dirname(template_path), "meta.json")
|
||||||
|
if os.path.isfile(meta_file):
|
||||||
|
with open(meta_file, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def generate(data, template, out_path, style_ref=None):
|
||||||
|
"""生成报价文档。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
data: QuoteData dict(或 JSON 文件路径字符串)
|
||||||
|
template: 模板 docx 文件路径
|
||||||
|
out_path: 输出 docx 路径
|
||||||
|
style_ref: 用户上传的样式参考 docx 路径(可选)
|
||||||
|
|
||||||
|
返回:
|
||||||
|
生成的 docx 绝对路径
|
||||||
|
"""
|
||||||
|
data = _load_data(data)
|
||||||
|
|
||||||
|
if not isinstance(template, str):
|
||||||
|
raise TypeError(f"template 必须是文件路径字符串,实际类型: {type(template).__name__}")
|
||||||
|
if not os.path.isfile(template):
|
||||||
|
raise FileNotFoundError(f"模板文件不存在: {template}")
|
||||||
|
template_path = os.path.abspath(template)
|
||||||
|
|
||||||
|
# 归一化 + 校验
|
||||||
|
data = normalize(data)
|
||||||
|
errs = validate(data)
|
||||||
|
if errs:
|
||||||
|
raise ValueError("数据校验失败:\n" + "\n".join(errs))
|
||||||
|
|
||||||
|
# 确保输出目录存在
|
||||||
|
out_dir = os.path.dirname(os.path.abspath(out_path))
|
||||||
|
if out_dir:
|
||||||
|
os.makedirs(out_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 渲染(优先读取同目录 meta.json 作为图片配置)
|
||||||
|
meta = _load_meta_for_template(template_path)
|
||||||
|
render(data, out_path, template_path, meta=meta)
|
||||||
|
|
||||||
|
# 可选样式迁移
|
||||||
|
if style_ref:
|
||||||
|
if not os.path.isfile(style_ref):
|
||||||
|
raise FileNotFoundError(f"样式参考文件不存在: {style_ref}")
|
||||||
|
transplant_style(out_path, style_ref, out_path)
|
||||||
|
|
||||||
|
return os.path.abspath(out_path)
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
render_quote.py:渲染引擎 — data + 模板路径 -> docx。
|
||||||
|
|
||||||
|
从 reports4 移植,做了两处通用化改造:
|
||||||
|
1. 模板路径参数化: render(data, out_path, template_path)
|
||||||
|
2. 图片字段配置驱动: 读 meta.json 的 image_fields 声明,不再写死字段名
|
||||||
|
"""
|
||||||
|
import copy
|
||||||
|
import os
|
||||||
|
import ssl
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
import urllib.request
|
||||||
|
import json
|
||||||
|
|
||||||
|
try:
|
||||||
|
sys.stdout.reconfigure(encoding="utf-8")
|
||||||
|
except (AttributeError, OSError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
from docxtpl import DocxTemplate, InlineImage
|
||||||
|
from docx.shared import Mm
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_image_path(src, tmp_files):
|
||||||
|
"""把图片字段值解析为本地文件路径。
|
||||||
|
|
||||||
|
下载成功的临时文件会记录到 tmp_files,由调用方在渲染结束后统一清理。
|
||||||
|
"""
|
||||||
|
def _download(url):
|
||||||
|
try:
|
||||||
|
ctx = ssl.create_default_context()
|
||||||
|
ctx.check_hostname = False
|
||||||
|
ctx.verify_mode = ssl.CERT_NONE
|
||||||
|
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||||
|
data = urllib.request.urlopen(req, context=ctx, timeout=30).read()
|
||||||
|
ext = os.path.splitext(url.split("?")[0])[1] or ".png"
|
||||||
|
fd, path = tempfile.mkstemp(suffix=ext)
|
||||||
|
os.write(fd, data)
|
||||||
|
os.close(fd)
|
||||||
|
tmp_files.append(path)
|
||||||
|
return path
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[warn] 下载图片失败({url}): {e}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
if src is None:
|
||||||
|
return ""
|
||||||
|
if src == "":
|
||||||
|
return ""
|
||||||
|
if isinstance(src, str) and src.lower().startswith(("http://", "https://")):
|
||||||
|
return _download(src) or ""
|
||||||
|
if os.path.exists(src):
|
||||||
|
return src
|
||||||
|
print(f"[warn] 找不到图片({src}),跳过")
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _make_inline_image(tpl, src, width_mm, tmp_files):
|
||||||
|
"""创建 InlineImage;src 为 "" 或 None 时不创建(返回 None)。"""
|
||||||
|
resolved = _resolve_image_path(src, tmp_files)
|
||||||
|
if resolved == "":
|
||||||
|
return None
|
||||||
|
return InlineImage(tpl, resolved, width=Mm(width_mm))
|
||||||
|
|
||||||
|
|
||||||
|
def _resolve_field_path(data, path):
|
||||||
|
"""根据字段路径提取所有目标值。
|
||||||
|
|
||||||
|
支持: "root_field" / "list[].field" / "list[].field[]"
|
||||||
|
"""
|
||||||
|
results = []
|
||||||
|
parts = path.split(".")
|
||||||
|
|
||||||
|
if len(parts) == 1:
|
||||||
|
key = parts[0]
|
||||||
|
if key in data:
|
||||||
|
results.append((data, key))
|
||||||
|
return results
|
||||||
|
|
||||||
|
# 嵌套列表: "list[].field[]" —— 必须先于单层判断,否则会被 parts[0] 分支误捕获
|
||||||
|
if len(parts) == 2 and "[]" in parts[1]:
|
||||||
|
list_name = parts[0].replace("[]", "")
|
||||||
|
field = parts[1].replace("[]", "")
|
||||||
|
for item in data.get(list_name, []):
|
||||||
|
lst = item.get(field, [])
|
||||||
|
if isinstance(lst, list):
|
||||||
|
for idx in range(len(lst)):
|
||||||
|
results.append((lst, idx))
|
||||||
|
return results
|
||||||
|
|
||||||
|
if len(parts) == 2 and "[]" in parts[0]:
|
||||||
|
list_name = parts[0].replace("[]", "")
|
||||||
|
field = parts[1]
|
||||||
|
for item in data.get(list_name, []):
|
||||||
|
if field in item:
|
||||||
|
results.append((item, field))
|
||||||
|
return results
|
||||||
|
|
||||||
|
if len(parts) == 2:
|
||||||
|
list_name = parts[0]
|
||||||
|
field = parts[1]
|
||||||
|
for item in data.get(list_name, []):
|
||||||
|
if field in item:
|
||||||
|
results.append((item, field))
|
||||||
|
return results
|
||||||
|
|
||||||
|
return results
|
||||||
|
|
||||||
|
|
||||||
|
def _fill_images_from_meta(tpl, data, meta, tmp_files):
|
||||||
|
"""根据 meta.json 中的 image_fields 声明填充所有图片字段。"""
|
||||||
|
image_fields = meta.get("image_fields", {})
|
||||||
|
for path, width_meta in image_fields.items():
|
||||||
|
width_mm = width_meta.get("width_mm", 50)
|
||||||
|
refs = _resolve_field_path(data, path)
|
||||||
|
for container, key in refs:
|
||||||
|
val = container[key]
|
||||||
|
if val is None:
|
||||||
|
container[key] = ""
|
||||||
|
elif isinstance(val, list):
|
||||||
|
container[key] = [
|
||||||
|
_make_inline_image(tpl, x, width_mm, tmp_files) if x is not None else None
|
||||||
|
for x in val
|
||||||
|
]
|
||||||
|
container[key] = [img for img in container[key] if img is not None]
|
||||||
|
else:
|
||||||
|
img = _make_inline_image(tpl, val, width_mm, tmp_files)
|
||||||
|
container[key] = img if img else ""
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def render(data, out_path, template_path, meta=None):
|
||||||
|
"""渲染报价文档。"""
|
||||||
|
if meta is None:
|
||||||
|
meta_dir = os.path.dirname(template_path)
|
||||||
|
meta_file = os.path.join(meta_dir, "meta.json")
|
||||||
|
if os.path.isfile(meta_file):
|
||||||
|
with open(meta_file, "r", encoding="utf-8") as f:
|
||||||
|
meta = json.load(f)
|
||||||
|
else:
|
||||||
|
meta = {}
|
||||||
|
|
||||||
|
# 深拷贝:避免修改调用方传入的数据,同时把字符串字段替换成 InlineImage
|
||||||
|
data = copy.deepcopy(dict(data))
|
||||||
|
tpl = DocxTemplate(template_path)
|
||||||
|
|
||||||
|
tmp_files = []
|
||||||
|
try:
|
||||||
|
data = _fill_images_from_meta(tpl, data, meta, tmp_files)
|
||||||
|
tpl.render(data)
|
||||||
|
tpl.save(out_path)
|
||||||
|
finally:
|
||||||
|
for p in tmp_files:
|
||||||
|
try:
|
||||||
|
os.remove(p)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
return out_path
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
schema.py:QuoteData 数据契约、校验器、归一化。
|
||||||
|
|
||||||
|
所有模板共用同一契约,沿用 reports4 的 SAMPLE 字段结构。
|
||||||
|
"""
|
||||||
|
import copy
|
||||||
|
|
||||||
|
# ── 缺省值表 ──────────────────────────────────────────────
|
||||||
|
DEFAULTS = {
|
||||||
|
"layout_title": "整线布局尺寸图",
|
||||||
|
"show_layout": True,
|
||||||
|
}
|
||||||
|
|
||||||
|
# 中文数字扩展(用于缺 index 时自动补)
|
||||||
|
_CN_DIGITS = [
|
||||||
|
"一", "二", "三", "四", "五", "六", "七", "八", "九", "十",
|
||||||
|
"十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十",
|
||||||
|
]
|
||||||
|
|
||||||
|
# 设备之前的固定章节数:一 公司简介、二 客户要求及分析、三 布局图
|
||||||
|
_SECTION_OFFSET = 3
|
||||||
|
|
||||||
|
|
||||||
|
def validate(data):
|
||||||
|
"""校验 QuoteData 结构,返回错误信息列表(空=通过)。
|
||||||
|
|
||||||
|
检查:
|
||||||
|
- 必填顶层字段: project_title, contact_person, contact_phone, requirements
|
||||||
|
- equipments 元素: 必须有 name
|
||||||
|
- quote_items 元素: 必须有 name
|
||||||
|
- features 元素: 必须有 title 和 lines
|
||||||
|
- params 元素: 必须有 k 和 v
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
for field in ("project_title", "contact_person", "contact_phone", "requirements"):
|
||||||
|
val = data.get(field)
|
||||||
|
if val is None or (isinstance(val, str) and val.strip() == ""):
|
||||||
|
errors.append(f"必填字段缺失或为空: {field}")
|
||||||
|
|
||||||
|
# equipments
|
||||||
|
eqs = data.get("equipments", [])
|
||||||
|
if not isinstance(eqs, list):
|
||||||
|
errors.append("equipments 必须为列表")
|
||||||
|
else:
|
||||||
|
for i, eq in enumerate(eqs):
|
||||||
|
if not isinstance(eq, dict):
|
||||||
|
errors.append(f"equipments[{i}] 必须为对象")
|
||||||
|
continue
|
||||||
|
if not eq.get("name"):
|
||||||
|
errors.append(f"equipments[{i}] 缺少 name")
|
||||||
|
# features 子元素
|
||||||
|
for j, feat in enumerate(eq.get("features", [])):
|
||||||
|
if not isinstance(feat, dict):
|
||||||
|
errors.append(f"equipments[{i}].features[{j}] 必须为对象")
|
||||||
|
continue
|
||||||
|
if not feat.get("title"):
|
||||||
|
errors.append(f"equipments[{i}].features[{j}] 缺少 title")
|
||||||
|
if not feat.get("lines") or not isinstance(feat.get("lines"), list):
|
||||||
|
errors.append(f"equipments[{i}].features[{j}] 缺少 lines 或类型错误")
|
||||||
|
# params 子元素
|
||||||
|
for j, p in enumerate(eq.get("params", [])):
|
||||||
|
if not isinstance(p, dict):
|
||||||
|
errors.append(f"equipments[{i}].params[{j}] 必须为对象")
|
||||||
|
continue
|
||||||
|
if not p.get("k") or not p.get("v"):
|
||||||
|
errors.append(f"equipments[{i}].params[{j}] 缺少 k 或 v")
|
||||||
|
|
||||||
|
# quote_items
|
||||||
|
items = data.get("quote_items", [])
|
||||||
|
if not isinstance(items, list):
|
||||||
|
errors.append("quote_items 必须为列表")
|
||||||
|
else:
|
||||||
|
for i, it in enumerate(items):
|
||||||
|
if not isinstance(it, dict):
|
||||||
|
errors.append(f"quote_items[{i}] 必须为对象")
|
||||||
|
continue
|
||||||
|
if not it.get("name"):
|
||||||
|
errors.append(f"quote_items[{i}] 缺少 name")
|
||||||
|
|
||||||
|
return errors
|
||||||
|
|
||||||
|
|
||||||
|
def normalize(data):
|
||||||
|
"""归一化数据:填缺省值、requirements list->str、index 缺失时自动补。
|
||||||
|
|
||||||
|
不修改入参,返回新副本。
|
||||||
|
"""
|
||||||
|
d = copy.deepcopy(data)
|
||||||
|
|
||||||
|
# requirements: list -> str
|
||||||
|
req = d.get("requirements")
|
||||||
|
if isinstance(req, (list, tuple)):
|
||||||
|
d["requirements"] = "\n".join(str(x) for x in req)
|
||||||
|
|
||||||
|
# 缺省值
|
||||||
|
for k, v in DEFAULTS.items():
|
||||||
|
if k not in d:
|
||||||
|
d[k] = v
|
||||||
|
|
||||||
|
# equipment index 自动补(从"四"开始,前面三节是公司简介/客户要求/布局图)
|
||||||
|
for i, eq in enumerate(d.get("equipments", [])):
|
||||||
|
if not eq.get("index"):
|
||||||
|
idx = i + 1 + _SECTION_OFFSET
|
||||||
|
eq["index"] = _CN_DIGITS[idx - 1] if idx <= len(_CN_DIGITS) else str(idx)
|
||||||
|
|
||||||
|
# quote_items 缺 image 给空串
|
||||||
|
for it in d.get("quote_items", []):
|
||||||
|
it.setdefault("image", "")
|
||||||
|
|
||||||
|
# equipments 缺 images 给 [""]
|
||||||
|
for eq in d.get("equipments", []):
|
||||||
|
eq.setdefault("images", [""])
|
||||||
|
|
||||||
|
# 计算后续章节的动态序号:报价表 & 售后服务
|
||||||
|
eq_count = len(d.get("equipments", []))
|
||||||
|
quote_idx = eq_count + _SECTION_OFFSET + 1
|
||||||
|
after_sales_idx = eq_count + _SECTION_OFFSET + 2
|
||||||
|
d["section_quote_table"] = _CN_DIGITS[quote_idx - 1] if quote_idx <= len(_CN_DIGITS) else str(quote_idx)
|
||||||
|
d["section_after_sales"] = _CN_DIGITS[after_sales_idx - 1] if after_sales_idx <= len(_CN_DIGITS) else str(after_sales_idx)
|
||||||
|
|
||||||
|
return d
|
||||||
@@ -0,0 +1,223 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
lzwcai-mcpskills-generate-reports MCP Server
|
||||||
|
|
||||||
|
把 docx 模板渲染引擎封装成 MCP 工具,提供三个工具:
|
||||||
|
- generate_report: 数据 + 模板路径 -> 渲染输出 docx
|
||||||
|
- scan_template: 扫描模板占位符 / for / if 块
|
||||||
|
- validate_report_data: 校验数据契约(不渲染)
|
||||||
|
|
||||||
|
stdio 模式运行;所有日志走 stderr,stdout 留给 MCP 协议。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
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
|
||||||
|
|
||||||
|
try:
|
||||||
|
from .utils.logger_config import setup_system_logging, get_logger
|
||||||
|
from . import generate, scan_template, validate, normalize
|
||||||
|
except ImportError:
|
||||||
|
from lzwcai_mcpskills_generate_reports.utils.logger_config import (
|
||||||
|
setup_system_logging, get_logger,
|
||||||
|
)
|
||||||
|
from lzwcai_mcpskills_generate_reports import (
|
||||||
|
generate, scan_template, validate, normalize,
|
||||||
|
)
|
||||||
|
|
||||||
|
# 初始化日志系统
|
||||||
|
setup_system_logging(app_name="lzwcai_mcpskills_generate_reports", log_level=logging.DEBUG)
|
||||||
|
logger = get_logger(__name__)
|
||||||
|
|
||||||
|
# 初始化 MCP Server
|
||||||
|
server = Server("lzwcai_mcpskills_generate_reports")
|
||||||
|
|
||||||
|
|
||||||
|
def _load_data(data):
|
||||||
|
"""把 data 归一化为 dict:支持 dict 或 JSON 文件路径字符串。"""
|
||||||
|
if isinstance(data, str):
|
||||||
|
if not os.path.isfile(data):
|
||||||
|
raise FileNotFoundError(f"数据文件不存在: {data}")
|
||||||
|
with open(data, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
raise TypeError(
|
||||||
|
f"data 必须是对象或 JSON 文件路径字符串,实际类型: {type(data).__name__}"
|
||||||
|
)
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
# ── 工具定义 ──────────────────────────────────────────────
|
||||||
|
_DATA_DESC = "报价数据:可以是 JSON 对象,或指向 JSON 文件的路径字符串"
|
||||||
|
|
||||||
|
TOOL_DEFS = [
|
||||||
|
types.Tool(
|
||||||
|
name="generate_report",
|
||||||
|
description=(
|
||||||
|
"用 docx 模板 + 结构化数据渲染生成报价文档 docx,返回输出文件绝对路径。"
|
||||||
|
"模板由调用方提供,本工具不内置模板。"
|
||||||
|
),
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"template": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "模板 docx 文件路径(必填)",
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"type": ["object", "string"],
|
||||||
|
"description": _DATA_DESC + "(必填)",
|
||||||
|
},
|
||||||
|
"out": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "输出 docx 文件路径(必填)",
|
||||||
|
},
|
||||||
|
"style_ref": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "用户上传的样式参考 docx 路径(可选),会把其 theme/字体套到结果文档",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["template", "data", "out"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="scan_template",
|
||||||
|
description=(
|
||||||
|
"扫描 docx 模板中的 Jinja2 占位符,返回需要外部提供的顶层变量列表和 for/if 块结构。"
|
||||||
|
"用于在渲染前了解模板需要哪些数据字段。"
|
||||||
|
),
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"template": {
|
||||||
|
"type": "string",
|
||||||
|
"description": "模板 docx 文件路径(必填)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["template"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
types.Tool(
|
||||||
|
name="validate_report_data",
|
||||||
|
description=(
|
||||||
|
"校验报价数据是否符合契约(必填字段、equipments/quote_items/features/params 结构),"
|
||||||
|
"不渲染文档。返回校验结果和错误列表。"
|
||||||
|
),
|
||||||
|
inputSchema={
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"data": {
|
||||||
|
"type": ["object", "string"],
|
||||||
|
"description": _DATA_DESC + "(必填)",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": ["data"],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@server.list_tools()
|
||||||
|
async def handle_list_tools() -> list[types.Tool]:
|
||||||
|
"""列出所有可用工具"""
|
||||||
|
logger.info(f"收到 ListTools 请求,返回 {len(TOOL_DEFS)} 个工具")
|
||||||
|
return TOOL_DEFS
|
||||||
|
|
||||||
|
|
||||||
|
# ── 工具实现(同步函数,放线程池执行)──────────────────────
|
||||||
|
def _do_generate_report(arguments: dict) -> dict:
|
||||||
|
template = arguments["template"]
|
||||||
|
data = arguments["data"]
|
||||||
|
out = arguments["out"]
|
||||||
|
style_ref = arguments.get("style_ref")
|
||||||
|
|
||||||
|
data = _load_data(data)
|
||||||
|
out_path = generate(data=data, template=template, out_path=out, style_ref=style_ref)
|
||||||
|
return {"output": out_path}
|
||||||
|
|
||||||
|
|
||||||
|
def _do_scan_template(arguments: dict) -> dict:
|
||||||
|
return scan_template(arguments["template"])
|
||||||
|
|
||||||
|
|
||||||
|
def _do_validate(arguments: dict) -> dict:
|
||||||
|
data = _load_data(arguments["data"])
|
||||||
|
norm = normalize(data)
|
||||||
|
errors = validate(norm)
|
||||||
|
return {"valid": not errors, "errors": errors}
|
||||||
|
|
||||||
|
|
||||||
|
_HANDLERS = {
|
||||||
|
"generate_report": _do_generate_report,
|
||||||
|
"scan_template": _do_scan_template,
|
||||||
|
"validate_report_data": _do_validate,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@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}, "
|
||||||
|
f"arguments={json.dumps(arguments, ensure_ascii=False) if arguments else 'None'}"
|
||||||
|
)
|
||||||
|
|
||||||
|
handler = _HANDLERS.get(name)
|
||||||
|
if handler is None:
|
||||||
|
logger.error(f"未找到工具: {name}")
|
||||||
|
raise ValueError(f"未知工具: {name}")
|
||||||
|
|
||||||
|
args = arguments or {}
|
||||||
|
try:
|
||||||
|
# 渲染/扫描是同步阻塞的 CPU/IO 任务,放线程池避免霸占 event loop
|
||||||
|
result = await anyio.to_thread.run_sync(handler, args)
|
||||||
|
logger.info(f"工具执行成功: {name}")
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"工具执行失败: {name}: {e}", exc_info=True)
|
||||||
|
result = {"error": str(e), "tool_name": name}
|
||||||
|
|
||||||
|
return [
|
||||||
|
types.TextContent(
|
||||||
|
type="text",
|
||||||
|
text=json.dumps(result, ensure_ascii=False, indent=2),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def run_server():
|
||||||
|
"""运行 MCP Server (stdio 模式)"""
|
||||||
|
async with stdio_server() as streams:
|
||||||
|
await server.run(
|
||||||
|
streams[0],
|
||||||
|
streams[1],
|
||||||
|
InitializationOptions(
|
||||||
|
server_name="lzwcai_mcpskills_generate_reports",
|
||||||
|
server_version="0.1.0",
|
||||||
|
capabilities=server.get_capabilities(
|
||||||
|
notification_options=NotificationOptions(),
|
||||||
|
experimental_capabilities={},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
"""主入口"""
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info("lzwcai-mcpskills-generate-reports MCP Server 启动")
|
||||||
|
logger.info("=" * 50)
|
||||||
|
logger.info("开始运行 MCP Server (stdio 模式)")
|
||||||
|
anyio.run(run_server)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
style_transfer.py:从用户上传的 docx 抽取视觉样式(theme1.xml + styles.xml 的默认字体引用),
|
||||||
|
套到内置骨架渲染结果上,产出"风格接近用户模板"的文档。
|
||||||
|
|
||||||
|
Level 1 (MVP):整体替换 theme + 默认字体引用,不整体覆盖 styles(避免版式崩)。
|
||||||
|
"""
|
||||||
|
import os
|
||||||
|
import zipfile
|
||||||
|
import tempfile
|
||||||
|
import shutil
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
# Word OOXML 命名空间
|
||||||
|
_NS = {
|
||||||
|
"a": "http://schemas.openxmlformats.org/drawingml/2006/main",
|
||||||
|
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships",
|
||||||
|
"w": "http://schemas.openxmlformats.org/wordprocessingml/2006/main",
|
||||||
|
}
|
||||||
|
|
||||||
|
# 主题字体引用中 major/minor 属性的前缀
|
||||||
|
_THEME_PREFIXES = ("majorHAnsi", "minorHAnsi", "majorEastAsia", "minorEastAsia",
|
||||||
|
"majorCs", "minorCs", "major", "minor")
|
||||||
|
|
||||||
|
|
||||||
|
def _unzip_docx(docx_path, target_dir):
|
||||||
|
"""解压 docx(zip)到目标目录。"""
|
||||||
|
with zipfile.ZipFile(docx_path, "r") as z:
|
||||||
|
z.extractall(target_dir)
|
||||||
|
|
||||||
|
|
||||||
|
def _zip_docx(source_dir, docx_path):
|
||||||
|
"""将目录内容打包为 docx(zip)。"""
|
||||||
|
with zipfile.ZipFile(docx_path, "w", zipfile.ZIP_DEFLATED) as z:
|
||||||
|
for root, _dirs, files in os.walk(source_dir):
|
||||||
|
for fname in files:
|
||||||
|
full = os.path.join(root, fname)
|
||||||
|
arcname = os.path.relpath(full, source_dir)
|
||||||
|
z.write(full, arcname)
|
||||||
|
|
||||||
|
|
||||||
|
def _safe_xml_read(xml_path):
|
||||||
|
"""安全读取 XML 文件,返回 ElementTree 和根元素。失败返回 (None, None)。"""
|
||||||
|
try:
|
||||||
|
tree = ET.parse(xml_path)
|
||||||
|
return tree, tree.getroot()
|
||||||
|
except Exception:
|
||||||
|
return None, None
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_theme(src_dir, dst_dir):
|
||||||
|
"""将 src 的 word/theme/theme1.xml 复制到 dst。"""
|
||||||
|
src_theme = os.path.join(src_dir, "word", "theme", "theme1.xml")
|
||||||
|
dst_theme = os.path.join(dst_dir, "word", "theme", "theme1.xml")
|
||||||
|
if os.path.isfile(src_theme):
|
||||||
|
os.makedirs(os.path.dirname(dst_theme), exist_ok=True)
|
||||||
|
shutil.copy2(src_theme, dst_theme)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _copy_font_table(src_dir, dst_dir):
|
||||||
|
"""将 src 的 word/fontTable.xml 复制到 dst。"""
|
||||||
|
src_ft = os.path.join(src_dir, "word", "fontTable.xml")
|
||||||
|
dst_ft = os.path.join(dst_dir, "word", "fontTable.xml")
|
||||||
|
if os.path.isfile(src_ft):
|
||||||
|
shutil.copy2(src_ft, dst_ft)
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_default_fonts(src_styles_path, dst_styles_path):
|
||||||
|
"""合并默认字体引用(docDefaults 中的 rFonts)从 src 到 dst。
|
||||||
|
|
||||||
|
不整体覆盖 styles.xml,只替换 docDefaults 里的字体引用。
|
||||||
|
"""
|
||||||
|
if not os.path.isfile(src_styles_path) or not os.path.isfile(dst_styles_path):
|
||||||
|
return False
|
||||||
|
|
||||||
|
_, src_root = _safe_xml_read(src_styles_path)
|
||||||
|
dst_tree, dst_root = _safe_xml_read(dst_styles_path)
|
||||||
|
if src_root is None or dst_root is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
w_ns = _NS["w"]
|
||||||
|
|
||||||
|
# 找 src 的 docDefaults
|
||||||
|
src_doc_defaults = src_root.find(f".//{{{w_ns}}}docDefaults")
|
||||||
|
if src_doc_defaults is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
src_rpr = src_doc_defaults.find(f"{{{w_ns}}}rPrDefault/{{{w_ns}}}rFonts")
|
||||||
|
if src_rpr is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 找 dst 的 docDefaults
|
||||||
|
dst_doc_defaults = dst_root.find(f".//{{{w_ns}}}docDefaults")
|
||||||
|
if dst_doc_defaults is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
dst_rpr = dst_doc_defaults.find(f"{{{w_ns}}}rPrDefault/{{{w_ns}}}rFonts")
|
||||||
|
if dst_rpr is None:
|
||||||
|
# 如果 dst 没有 rFonts,创建并追加
|
||||||
|
rpr_default = dst_doc_defaults.find(f"{{{w_ns}}}rPrDefault")
|
||||||
|
if rpr_default is None:
|
||||||
|
rpr_default = ET.SubElement(dst_doc_defaults, f"{{{w_ns}}}rPrDefault")
|
||||||
|
rpr_default.append(src_rpr)
|
||||||
|
else:
|
||||||
|
# 替换 rFonts 的属性(主题字体引用)
|
||||||
|
for attr_name, attr_val in src_rpr.attrib.items():
|
||||||
|
if any(attr_name.endswith(prefix) for prefix in _THEME_PREFIXES):
|
||||||
|
dst_rpr.set(attr_name, attr_val)
|
||||||
|
|
||||||
|
# 保存
|
||||||
|
dst_tree.write(dst_styles_path, xml_declaration=True, encoding="UTF-8", standalone="yes")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def transplant_style(result_path, user_template_path, out_path):
|
||||||
|
"""将用户模板的视觉样式移植到结果文档上。
|
||||||
|
|
||||||
|
参数:
|
||||||
|
result_path: 内置骨架渲染出的 docx 路径
|
||||||
|
user_template_path: 用户上传的样式参考 docx
|
||||||
|
out_path: 输出路径(可与 result_path 相同,原地覆盖)
|
||||||
|
"""
|
||||||
|
# 校验用户模板可打开
|
||||||
|
try:
|
||||||
|
from docx import Document
|
||||||
|
Document(user_template_path)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[warn] 用户上传模板无法打开,跳过样式迁移: {e}")
|
||||||
|
if out_path != result_path:
|
||||||
|
shutil.copy2(result_path, out_path)
|
||||||
|
return out_path
|
||||||
|
|
||||||
|
# 在打包覆盖前先记录原文档段落数(out_path 可能 == result_path,原地覆盖后就读不到原值了)
|
||||||
|
src_count = None
|
||||||
|
try:
|
||||||
|
from docx import Document
|
||||||
|
src_count = len(Document(result_path).paragraphs)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[warn] 读取原文档段落数失败,迁移后将跳过段落数校验: {e}")
|
||||||
|
|
||||||
|
# 创建临时工作目录
|
||||||
|
with tempfile.TemporaryDirectory() as tmpdir:
|
||||||
|
src_dir = os.path.join(tmpdir, "src") # 用户上传模板
|
||||||
|
dst_dir = os.path.join(tmpdir, "dst") # 渲染结果
|
||||||
|
os.makedirs(src_dir)
|
||||||
|
os.makedirs(dst_dir)
|
||||||
|
|
||||||
|
# 解压
|
||||||
|
_unzip_docx(user_template_path, src_dir)
|
||||||
|
_unzip_docx(result_path, dst_dir)
|
||||||
|
|
||||||
|
# 1) 替换 theme1.xml
|
||||||
|
_copy_theme(src_dir, dst_dir)
|
||||||
|
|
||||||
|
# 2) 替换 fontTable.xml
|
||||||
|
_copy_font_table(src_dir, dst_dir)
|
||||||
|
|
||||||
|
# 3) 合并默认字体引用
|
||||||
|
src_styles = os.path.join(src_dir, "word", "styles.xml")
|
||||||
|
dst_styles = os.path.join(dst_dir, "word", "styles.xml")
|
||||||
|
_merge_default_fonts(src_styles, dst_styles)
|
||||||
|
|
||||||
|
# 重新打包
|
||||||
|
_zip_docx(dst_dir, out_path)
|
||||||
|
|
||||||
|
# 校验:能正常打开且段落数与迁移前一致
|
||||||
|
try:
|
||||||
|
from docx import Document
|
||||||
|
dst_count = len(Document(out_path).paragraphs)
|
||||||
|
if src_count is None:
|
||||||
|
print(f"[ok] 样式迁移完成(无原始段落数可比对),输出段落数: {dst_count}")
|
||||||
|
elif dst_count == 0 or dst_count != src_count:
|
||||||
|
print(f"[warn] 样式迁移后段落数不一致 (src={src_count}, dst={dst_count}),可能损坏")
|
||||||
|
else:
|
||||||
|
print(f"[ok] 样式迁移完成,段落数一致: {src_count}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[error] 样式迁移后文档无法打开: {e}")
|
||||||
|
|
||||||
|
return out_path
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
template_scanner.py:扫描 docx 模板中的 Jinja2 占位符。
|
||||||
|
|
||||||
|
只读模板,不渲染;返回模板里要求外部提供的数据字段清单。
|
||||||
|
"""
|
||||||
|
import re
|
||||||
|
import zipfile
|
||||||
|
import xml.etree.ElementTree as ET
|
||||||
|
|
||||||
|
from jinja2 import Environment, meta
|
||||||
|
from jinja2 import nodes as jinja_nodes
|
||||||
|
|
||||||
|
|
||||||
|
def _iter_docx_text(docx_path):
|
||||||
|
"""遍历 docx 中所有 XML 文本节点,产出原始字符串片段。"""
|
||||||
|
with zipfile.ZipFile(docx_path, "r") as z:
|
||||||
|
for name in z.namelist():
|
||||||
|
# 只关心 word 主文档、页眉、页脚
|
||||||
|
if not (name.startswith("word/document") or name.startswith("word/header") or name.startswith("word/footer")):
|
||||||
|
continue
|
||||||
|
data = z.read(name)
|
||||||
|
try:
|
||||||
|
root = ET.fromstring(data)
|
||||||
|
except ET.ParseError:
|
||||||
|
continue
|
||||||
|
# w:t 节点存放文本
|
||||||
|
for elem in root.iter():
|
||||||
|
if elem.tag.endswith("}t"):
|
||||||
|
if elem.text:
|
||||||
|
yield elem.text
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_source(docx_path):
|
||||||
|
"""把 docx 所有文本片段拼成一段连续的源文本,便于 Jinja2 解析。"""
|
||||||
|
return "\n".join(_iter_docx_text(docx_path))
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_docxtpl_tags(source):
|
||||||
|
"""把 docxtpl 段落/行/单元格级标签 {%p ... %} {%tr ... %} {%tc ... %} 还原为 {% ... %}。
|
||||||
|
|
||||||
|
docxtpl 用 {%p、{%tr、{%tc 控制块作用于段落、表格行、表格单元格;
|
||||||
|
扫描占位符时不需要这些粒度信息,统一成标准 Jinja2 标签即可解析。
|
||||||
|
"""
|
||||||
|
return re.sub(r"{%\s*(?:p|tr|tc)\s+", "{% ", source)
|
||||||
|
|
||||||
|
|
||||||
|
def _walk_blocks(node, result=None):
|
||||||
|
"""遍历 Jinja2 AST,收集 For / If 块信息。"""
|
||||||
|
if result is None:
|
||||||
|
result = []
|
||||||
|
if node is None:
|
||||||
|
return result
|
||||||
|
|
||||||
|
if isinstance(node, jinja_nodes.For):
|
||||||
|
iter_name = _expression_name(node.iter)
|
||||||
|
target_name = _expression_name(node.target)
|
||||||
|
result.append({
|
||||||
|
"type": "for",
|
||||||
|
"iterator": target_name,
|
||||||
|
"variable": iter_name,
|
||||||
|
})
|
||||||
|
for child in node.body:
|
||||||
|
_walk_blocks(child, result)
|
||||||
|
for child in node.else_ or []:
|
||||||
|
_walk_blocks(child, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
if isinstance(node, jinja_nodes.If):
|
||||||
|
test_name = _expression_name(node.test)
|
||||||
|
result.append({
|
||||||
|
"type": "if",
|
||||||
|
"condition": test_name,
|
||||||
|
})
|
||||||
|
for child in node.body:
|
||||||
|
_walk_blocks(child, result)
|
||||||
|
for child in node.else_ or []:
|
||||||
|
_walk_blocks(child, result)
|
||||||
|
return result
|
||||||
|
|
||||||
|
if hasattr(node, "body"):
|
||||||
|
for child in node.body:
|
||||||
|
_walk_blocks(child, result)
|
||||||
|
|
||||||
|
if hasattr(node, "else_") and node.else_:
|
||||||
|
for child in node.else_:
|
||||||
|
_walk_blocks(child, result)
|
||||||
|
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _expression_name(expr):
|
||||||
|
"""把 Jinja2 表达式尽量还原为可读的字符串。"""
|
||||||
|
if expr is None:
|
||||||
|
return None
|
||||||
|
if isinstance(expr, jinja_nodes.Name):
|
||||||
|
return expr.name
|
||||||
|
if isinstance(expr, jinja_nodes.Const):
|
||||||
|
return str(expr.value)
|
||||||
|
if isinstance(expr, jinja_nodes.Getattr):
|
||||||
|
return f"{_expression_name(expr.node)}.{expr.attr}"
|
||||||
|
if isinstance(expr, jinja_nodes.Getitem):
|
||||||
|
return f"{_expression_name(expr.node)}[{_expression_name(expr.arg)}]"
|
||||||
|
return str(expr)
|
||||||
|
|
||||||
|
|
||||||
|
def scan_template(template_path):
|
||||||
|
"""扫描 docx 模板,返回占位符信息 JSON。
|
||||||
|
|
||||||
|
返回:
|
||||||
|
{
|
||||||
|
"placeholders": ["project_title", "equipments", ...], # 需要外部提供的最顶层变量
|
||||||
|
"blocks": [
|
||||||
|
{"type": "for", "iterator": "eq", "variable": "equipments"},
|
||||||
|
{"type": "if", "condition": "show_layout"},
|
||||||
|
...
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
source = _normalize_docxtpl_tags(_extract_source(template_path))
|
||||||
|
if not source.strip():
|
||||||
|
return {"placeholders": [], "blocks": []}
|
||||||
|
|
||||||
|
env = Environment()
|
||||||
|
ast = env.parse(source)
|
||||||
|
|
||||||
|
variables = sorted(meta.find_undeclared_variables(ast))
|
||||||
|
blocks = _walk_blocks(ast)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"placeholders": variables,
|
||||||
|
"blocks": blocks,
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""Utils package for lzwcai_mcpskills_generate_reports"""
|
||||||
|
|
||||||
|
from .logger_config import setup_system_logging, get_logger
|
||||||
|
|
||||||
|
__all__ = ["setup_system_logging", "get_logger"]
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
"""
|
||||||
|
统一日志配置模块
|
||||||
|
提供系统级别的日志配置和管理
|
||||||
|
|
||||||
|
注意:MCP 协议使用 stdio 通信时,stdout 被协议占用,
|
||||||
|
所有日志必须输出到 stderr 或文件,绝不能写 stdout。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
from logging.handlers import RotatingFileHandler, TimedRotatingFileHandler
|
||||||
|
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(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_mcpskills_generate_reports",
|
||||||
|
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. 控制台输出到 stderr(stdio 模式下 stdout 被 MCP 协议占用)
|
||||||
|
if console_output:
|
||||||
|
console_handler = logging.StreamHandler(sys.stderr)
|
||||||
|
console_handler.setLevel(log_level)
|
||||||
|
console_handler.setFormatter(logging.Formatter(
|
||||||
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||||
|
self.date_format,
|
||||||
|
))
|
||||||
|
root_logger.addHandler(console_handler)
|
||||||
|
|
||||||
|
self._initialized = True
|
||||||
|
root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}")
|
||||||
|
return root_logger
|
||||||
|
|
||||||
|
def get_module_logger(self, module_name: str) -> logging.Logger:
|
||||||
|
return logging.getLogger(module_name)
|
||||||
|
|
||||||
|
|
||||||
|
# 全局日志配置实例
|
||||||
|
logger_config = LoggerConfig()
|
||||||
|
|
||||||
|
|
||||||
|
def setup_system_logging(app_name: str = "lzwcai_mcpskills_generate_reports",
|
||||||
|
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)
|
||||||
10
lzwcai_mcpskills_generate_reports/main.py
Normal file
10
lzwcai_mcpskills_generate_reports/main.py
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
"""
|
||||||
|
Entry point for lzwcai-mcpskills-generate-reports
|
||||||
|
|
||||||
|
Runs the MCP server (stdio mode) for docx report generation.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from lzwcai_mcpskills_generate_reports.server import main
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
40
lzwcai_mcpskills_generate_reports/pyproject.toml
Normal file
40
lzwcai_mcpskills_generate_reports/pyproject.toml
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
[build-system]
|
||||||
|
requires = ["setuptools>=61.0", "wheel"]
|
||||||
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
|
[project]
|
||||||
|
name = "lzwcai-mcpskills-generate-reports"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Render styled quotation documents from user-supplied docx templates and structured data"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.12"
|
||||||
|
keywords = ["docx", "quotation", "report", "template", "jinja2"]
|
||||||
|
authors = [
|
||||||
|
{ name = "LzwCai", email = "lzwcai@example.com" },
|
||||||
|
]
|
||||||
|
classifiers = [
|
||||||
|
"Development Status :: 3 - Alpha",
|
||||||
|
"Intended Audience :: Developers",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.12",
|
||||||
|
]
|
||||||
|
dependencies = [
|
||||||
|
"docxtpl>=0.16.0",
|
||||||
|
"python-docx>=1.1.0",
|
||||||
|
"Jinja2>=3.1.0",
|
||||||
|
"mcp>=1.0.0",
|
||||||
|
"anyio>=4.0.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
generate-report = "lzwcai_mcpskills_generate_reports.cli:main"
|
||||||
|
lzwcai-mcpskills-generate-reports = "lzwcai_mcpskills_generate_reports.server:main"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
Homepage = "https://github.com/lzwcai/lzwcai-mcpskills-generate-reports"
|
||||||
|
Repository = "https://github.com/lzwcai/lzwcai-mcpskills-generate-reports"
|
||||||
|
|
||||||
|
[tool.setuptools.packages.find]
|
||||||
|
where = ["."]
|
||||||
|
include = ["lzwcai_mcpskills_generate_reports*"]
|
||||||
|
exclude = ["tests*", "_out*", "templates*", "samples*"]
|
||||||
412
lzwcai_mcpskills_generate_reports/samples/sample_data.json
Normal file
412
lzwcai_mcpskills_generate_reports/samples/sample_data.json
Normal file
@@ -0,0 +1,412 @@
|
|||||||
|
{
|
||||||
|
"project_title": "大米圆形纸罐全自动灌装包装整线项目",
|
||||||
|
"contact_person": "张卫国经理",
|
||||||
|
"contact_phone": "138-1568-9632",
|
||||||
|
"contact_company": "XX粮油食品有限公司",
|
||||||
|
"requirements": [
|
||||||
|
"包装物料:成品精制大米,流动性颗粒物料;单罐净重规格:250g、500g、1000g三档快速切换。",
|
||||||
|
"罐体参数:圆形食品级纸罐,配套易拉预封口+外旋螺纹盖双重密封;纸罐固定外径100mm(10cm),高度分3档:60mm(6cm)/120mm(12cm)/220mm(22cm),同线兼容三规格自动换型无需更换夹具。",
|
||||||
|
"额定包装速度:以500g标准罐计,稳定产能≥20罐/分钟,连续24h不间断运行无卡罐、漏装。",
|
||||||
|
"整线标配:大米定量称重灌装机、罐身输送线、自动上盖机、四轮旋盖机、成品出料输送机;",
|
||||||
|
"客户选配完整模组:在线金属检测机(带自动剔除机构)+重量复检机(不合格自动分流剔除)+后端自动开箱机+折盖封箱一体机+机器人码垛单元;",
|
||||||
|
"全线材质要求:物料接触部位304不锈钢,符合食品QS/SC生产卫生规范,支持水洗清洁。"
|
||||||
|
],
|
||||||
|
"layout_image": "https://dscache.tencent-cloud.cn/upload/nodir/rice-can-line-layout-202605.png",
|
||||||
|
"layout_title": "大米纸罐灌装整线平面布局尺寸总图",
|
||||||
|
"equipments": [
|
||||||
|
{
|
||||||
|
"name": "Z型大米颗粒上料提升机",
|
||||||
|
"images": [
|
||||||
|
"https://dscache.tencent-cloud.cn/upload/nodir/z-conveyor-01.png",
|
||||||
|
"https://dscache.tencent-cloud.cn/upload/nodir/z-conveyor-detail.png"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"title": "粮食专用密封提升",
|
||||||
|
"lines": [
|
||||||
|
"封闭式料斗输送,无大米撒料、扬尘,车间粉尘达标;适配大米、杂粮等颗粒原料连续上料。",
|
||||||
|
"料斗加厚304不锈钢,耐磨抗冲击,不易积粮霉变,便于高压水枪冲洗。"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "变频调速稳定可控",
|
||||||
|
"lines": [
|
||||||
|
"独立变频电机调速,上料流量与灌装主机信号联动匹配,不会断料或溢料。",
|
||||||
|
"低噪音链条传动,连续运行故障率低,维护简单,可长期满负荷生产。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"k": "有效提升高度",
|
||||||
|
"v": "3.6m(支持现场按需加长定制)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "输送速度可调范围",
|
||||||
|
"v": "0~16m/min"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "最大输送产能",
|
||||||
|
"v": "7.2m³/h,满足25罐/分钟灌装余量"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "驱动总功率",
|
||||||
|
"v": "0.55kW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "供电制式",
|
||||||
|
"v": "380V三相 50Hz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "整机净重",
|
||||||
|
"v": "385kg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "物料接触材质",
|
||||||
|
"v": "304食品级不锈钢"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "多头称重式大米灌装机",
|
||||||
|
"images": [
|
||||||
|
"https://dscache.tencent-cloud.cn/upload/nodir/rice-filling-machine.png"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"title": "高精度称重灌装",
|
||||||
|
"lines": [
|
||||||
|
"二级快慢加料,250g/500g/1000g重量参数触摸屏一键调用,单罐称重误差≤±0.8g。",
|
||||||
|
"独立称重料斗,不受物料料位高低影响,长时间灌装重量一致性稳定。"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "食品级卫生设计",
|
||||||
|
"lines": [
|
||||||
|
"料仓、下料口快拆结构,无需工具即可拆卸清洗,无死角存粮。",
|
||||||
|
"整机带防尘外封板,避免蚊虫、杂物混入大米成品。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"k": "灌装量程",
|
||||||
|
"v": "100~1200g可调"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "额定产能",
|
||||||
|
"v": "22~28罐/分钟(500g规格)"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "允许罐型外径",
|
||||||
|
"v": "60~120mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "整机功率",
|
||||||
|
"v": "1.2kW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "外形长宽高",
|
||||||
|
"v": "1450×920×1850mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "整机重量",
|
||||||
|
"v": "420kg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "操作界面",
|
||||||
|
"v": "7寸触摸屏+PLC全自动控制"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "自动纸罐理罐机",
|
||||||
|
"images": [
|
||||||
|
"https://dscache.tencent-cloud.cn/upload/nodir/can-sorter.png"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"title": "圆形纸罐定向排序",
|
||||||
|
"lines": [
|
||||||
|
"自动散乱上罐、扶正定位、有序输送,杜绝倒罐、卡罐,适配本次100mm外径纸罐三高度规格。",
|
||||||
|
"定位挡板手摇快速调节,换型时间≤3分钟。"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "整机耐用易维护",
|
||||||
|
"lines": [
|
||||||
|
"机架304不锈钢,输送带独立无极变频调速,可和灌装主机速度同步联动。",
|
||||||
|
"机械结构简洁,易损件少,车间操作工可独立日常检修。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"k": "主体材质",
|
||||||
|
"v": "304不锈钢机架+食品级PU输送带"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "整机外形尺寸",
|
||||||
|
"v": "1020×810×1220mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "最大处理产能",
|
||||||
|
"v": "30~50罐/分钟"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "理罐转盘直径",
|
||||||
|
"v": "800mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "适配罐体外径",
|
||||||
|
"v": "60~200mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "工作电压",
|
||||||
|
"v": "单相220V,50Hz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "整机重量",
|
||||||
|
"v": "145kg"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "自动上盖机+单头四轮旋盖一体机",
|
||||||
|
"images": [
|
||||||
|
"https://dscache.tencent-cloud.cn/upload/nodir/capping-twist-machine.png"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"title": "自动送盖+定位旋盖一体化",
|
||||||
|
"lines": [
|
||||||
|
"料仓自动整理螺纹外盖,分盖、落盖精准套入罐口,四轮橡胶轮柔性夹紧旋紧,不会压扁纸质罐口。",
|
||||||
|
"旋盖扭矩数字可调,杜绝滑盖、拧过紧纸罐变形,适配易拉内封+外旋盖双层封口工艺。"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "速度同步联动",
|
||||||
|
"lines": [
|
||||||
|
"变频调速跟随灌装线主线速度,无空罐漏旋,缺盖自动停机报警提示补料。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"k": "供电规格",
|
||||||
|
"v": "220V 50Hz"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "整机装机功率",
|
||||||
|
"v": "1.0kW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "适配罐口直径",
|
||||||
|
"v": "35~130mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "稳定旋盖速度",
|
||||||
|
"v": "25~32罐/分钟"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "设备外形尺寸",
|
||||||
|
"v": "2020×660×1510mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "整机重量",
|
||||||
|
"v": "295kg"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "扭矩调节方式",
|
||||||
|
"v": "数字扭矩电控调节"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "在线金属检测机(带剔除)",
|
||||||
|
"images": [
|
||||||
|
"https://dscache.tencent-cloud.cn/upload/nodir/metal-detector-reject.png"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"title": "高精度金属异物检测",
|
||||||
|
"lines": [
|
||||||
|
"可检出铁、不锈钢、铜、铝等混入大米内金属碎屑、螺钉刀片,不合格罐气动自动剔除分流,不混入合格品。",
|
||||||
|
"检测灵敏度数字可调,产品记忆存储,多规格一键切换。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"k": "检测通道尺寸",
|
||||||
|
"v": "宽140mm×高280mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "检测灵敏度",
|
||||||
|
"v": "Feφ1.0mm、SUSφ2.2mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "剔除方式",
|
||||||
|
"v": "气动推杆自动剔除"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "适配线速",
|
||||||
|
"v": "0~30m/min"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "功率",
|
||||||
|
"v": "0.37kW"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "成品重量复检机",
|
||||||
|
"images": [
|
||||||
|
"https://dscache.tencent-cloud.cn/upload/nodir/checkweigher.png"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"title": "缺料超重自动分选",
|
||||||
|
"lines": [
|
||||||
|
"在线动态称重,低于下限、高于上限罐体自动剔除,杜绝少装、多装次品流入装箱工序。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"k": "称重量程",
|
||||||
|
"v": "0~2000g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "称重精度",
|
||||||
|
"v": "±0.3g"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "剔除方式",
|
||||||
|
"v": "气动拨杆剔除"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "后端自动开箱+折盖封箱一体机",
|
||||||
|
"images": [
|
||||||
|
"https://dscache.tencent-cloud.cn/upload/nodir/case-sealer.png"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"title": "纸箱成型封底+上盖折平封箱一次完成",
|
||||||
|
"lines": [
|
||||||
|
"整垛纸箱自动吸取撑开、底部胶带封牢,成品罐装箱后自动折左右上盖,上下工字封箱,适配整线连续自动化装箱。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"k": "适用纸箱尺寸范围",
|
||||||
|
"v": "长250~450×宽180~320×高150~400mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "封箱速度",
|
||||||
|
"v": "6~12箱/分钟"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "总功率",
|
||||||
|
"v": "1.8kW"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "机械臂码垛机",
|
||||||
|
"images": [
|
||||||
|
"https://dscache.tencent-cloud.cn/upload/nodir/robot-palletizer.png"
|
||||||
|
],
|
||||||
|
"features": [
|
||||||
|
{
|
||||||
|
"title": "纸箱自动堆叠码垛",
|
||||||
|
"lines": [
|
||||||
|
"伺服抓手抓取成品整箱,按预设垛型整齐码放在托盘上,码垛高度、层数程序可调,替代人工堆垛。"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"params": [
|
||||||
|
{
|
||||||
|
"k": "最大负载",
|
||||||
|
"v": "25kg/箱"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "码垛高度上限",
|
||||||
|
"v": "1800mm"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"k": "工作节拍",
|
||||||
|
"v": "8~12箱/分钟"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"quote_items": [
|
||||||
|
{
|
||||||
|
"name": "Z型大米上料提升机",
|
||||||
|
"qty": "1套",
|
||||||
|
"image": "https://dscache.tencent-cloud.cn/upload/nodir/z-conveyor-01.png",
|
||||||
|
"desc": "304不锈钢封闭式粮食提升,变频调速,配套灌装主机联动上料",
|
||||||
|
"price": "面议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "多头称重大米灌装机",
|
||||||
|
"qty": "1套",
|
||||||
|
"image": "https://dscache.tencent-cloud.cn/upload/nodir/rice-filling-machine.png",
|
||||||
|
"desc": "三规格重量一键切换,高精度称重下料,食品级快拆清洗结构",
|
||||||
|
"price": "面议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "自动纸罐理罐机",
|
||||||
|
"qty": "1套",
|
||||||
|
"image": "https://dscache.tencent-cloud.cn/upload/nodir/can-sorter.png",
|
||||||
|
"desc": "圆形纸罐自动排序扶正,适配Φ100mm三高度纸罐快速换型",
|
||||||
|
"price": "面议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "自动上盖+四轮旋盖一体机",
|
||||||
|
"qty": "1套",
|
||||||
|
"image": "https://dscache.tencent-cloud.cn/upload/nodir/capping-twist-machine.png",
|
||||||
|
"desc": "自动分盖上盖,数字扭矩旋紧,适配纸罐易拉盖+外旋盖双层封口",
|
||||||
|
"price": "面议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "金属检测机(带剔除)",
|
||||||
|
"qty": "1套",
|
||||||
|
"image": "https://dscache.tencent-cloud.cn/upload/nodir/metal-detector-reject.png",
|
||||||
|
"desc": "在线金属异物检测,不合格罐体自动剔除分流,食品生产合规必备",
|
||||||
|
"price": "面议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "重量复检剔除机",
|
||||||
|
"qty": "1套",
|
||||||
|
"image": "https://dscache.tencent-cloud.cn/upload/nodir/checkweigher.png",
|
||||||
|
"desc": "动态在线称重,超重欠重次品自动剔除,保证净含量达标",
|
||||||
|
"price": "面议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "自动开箱折盖封箱一体机",
|
||||||
|
"qty": "1套",
|
||||||
|
"image": "https://dscache.tencent-cloud.cn/upload/nodir/case-sealer.png",
|
||||||
|
"desc": "纸箱自动成型封底、装箱后折盖封箱,后端自动化装箱配套",
|
||||||
|
"price": "面议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "机器人码垛单元",
|
||||||
|
"qty": "1套",
|
||||||
|
"image": "https://dscache.tencent-cloud.cn/upload/nodir/robot-palletizer.png",
|
||||||
|
"desc": "抓取成品纸箱自动码垛堆托,解放后端人工搬运堆叠",
|
||||||
|
"price": "面议"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "全线不锈钢输送过渡机架+电控总控制柜",
|
||||||
|
"qty": "1套",
|
||||||
|
"image": "https://dscache.tencent-cloud.cn/upload/nodir/line-conveyor-total.png",
|
||||||
|
"desc": "各设备接驳输送线、整机联动PLC总控制系统、急停、报警、联动互锁整套电气配套",
|
||||||
|
"price": "面议"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"name": "标准罐装线模板",
|
||||||
|
"description": "默认模板,适用于大多数罐装线报价方案",
|
||||||
|
"image_fields": {
|
||||||
|
"layout_image": {"width_mm": 160, "scope": "root"},
|
||||||
|
"equipments[].images[]": {"width_mm": 120, "scope": "list"},
|
||||||
|
"quote_items[].image": {"width_mm": 30, "scope": "list"}
|
||||||
|
},
|
||||||
|
"required": ["project_title", "contact_person", "contact_phone", "requirements"],
|
||||||
|
"optional_sections": ["show_layout"]
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user