Compare commits
2 Commits
research-f
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 635313a7ab | |||
| ba5cd4bbe1 |
Binary file not shown.
@@ -1,7 +1,8 @@
|
||||
---
|
||||
name: lzwcai-agile-db
|
||||
description: AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。
|
||||
version: 0.2.0
|
||||
metadata:
|
||||
version: 0.4.4
|
||||
---
|
||||
|
||||
# lzwcai-agile-db
|
||||
@@ -9,33 +10,56 @@ version: 0.2.0
|
||||
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` | 获取数据源列表 | 安全 |
|
||||
| `get_datasource_detail` | 获取数据源详情(含数据库、表结构) | 安全 |
|
||||
| `create_datasource` | 创建外部数据源连接 | 安全 |
|
||||
| `create_datasource` | 创建外部数据源连接(可选先测连接) | 安全 |
|
||||
| `update_datasource` | 更新数据源连接信息 | 中等 |
|
||||
| `toggle_datasource_status` | 启用/停用数据源 | 中等 |
|
||||
| `delete_datasource` | 删除数据源 | **危险** |
|
||||
| `test_connection` | 测试外部数据库连接(不创建) | 安全 |
|
||||
| `get_realtime_structure` | 实时探测远端库表结构 | 安全 |
|
||||
| `create_builtin_datasource` | 创建内置 PostgreSQL 连接(建库表链路第一步) | 安全 |
|
||||
| `update_builtin_datasource` | 修改内置 PostgreSQL 连接 | 中等 |
|
||||
|
||||
### 二、数据库与表管理(6 个工具)
|
||||
### 二、数据库与表管理(9 个工具)
|
||||
|
||||
| 工具 | 功能 | 危险等级 |
|
||||
|------|------|----------|
|
||||
| `list_databases` | 获取数据源下的数据库列表 | 安全 |
|
||||
| `list_tables` | 获取数据源下的表列表 | 安全 |
|
||||
| `list_tables` | 获取数据源下的表列表(基础) | 安全 |
|
||||
| `list_tables_with_ai` | 获取表元数据列表(含 AI 训练描述,内置库主列表) | 安全 |
|
||||
| `get_table_detail` | 获取表结构详情(字段、类型、主键) | 安全 |
|
||||
| `create_table` | 创建新表 | **危险** |
|
||||
| `alter_table` | 修改表结构 | **危险** |
|
||||
| `generate_table_by_description` | AI 根据自然语言生成表结构 | 安全(仅生成,不创建) |
|
||||
| `create_database` | 在内置源下创建数据库(建表前置) | **危险** |
|
||||
| `create_table` | 在指定库创建新表 | **危险** |
|
||||
| `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` | 删除数据行(按主键) | **危险** |
|
||||
| `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 智能识别并预览 | 安全 |
|
||||
| `confirm_import_data` | 确认导入 AI 识别后的数据 | **危险** |
|
||||
| `preview_import_data` | 从 URL 下载 Excel 文件,AI 智能识别并预览 | 安全 |
|
||||
| `confirm_import_data` | 确认导入(需传 databaseName,自动组装结构) | **危险** |
|
||||
|
||||
### 六、表订阅(1 个工具)
|
||||
### 七、表订阅(1 个工具)
|
||||
|
||||
| 工具 | 功能 | 危险等级 |
|
||||
|------|------|----------|
|
||||
| `toggle_table_subscription` | 切换表的订阅状态 | 中等 |
|
||||
| `toggle_table_subscription` | 切换表的订阅状态(configId+tableName+subscribe) | 中等 |
|
||||
|
||||
### 七、API 密钥管理(6 个工具)
|
||||
### 八、API 密钥管理(7 个工具)
|
||||
|
||||
| 工具 | 功能 | 危险等级 |
|
||||
|------|------|----------|
|
||||
@@ -73,18 +97,40 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
| `toggle_api_key_status` | 启用/禁用 API 密钥 | 中等 |
|
||||
| `delete_api_key` | 删除 API 密钥 | **危险** |
|
||||
| `get_api_key_permissions` | 查看指定密钥的权限配置 | 安全 |
|
||||
| `grant_api_key_permissions` | 批量为 API 密钥授予权限 | **危险** |
|
||||
| `grant_api_key_permissions` | 批量为 API 密钥授予权限(仅追加,不可撤销) | **危险** |
|
||||
|
||||
### 八、技能与工具管理(6 个工具)
|
||||
### 九、技能与工具管理(7 个工具)
|
||||
|
||||
| 工具 | 功能 | 危险等级 |
|
||||
|------|------|----------|
|
||||
| `add_sql_tool_to_datasource` | 把 SQL 沉淀为工具(一步到位,自动建技能+配模板+建工具,去重幂等)**唯一推荐入口** | 中等 |
|
||||
| `get_skill_by_datasource` | 根据数据源获取技能信息 | 安全 |
|
||||
| `get_skill_tools` | 获取技能下的工具列表 | 安全 |
|
||||
| `create_skill` | 为数据源创建技能 | 中等 |
|
||||
| `create_sql_tool` | 将 SQL 查询创建为可复用工具 | 中等 |
|
||||
| `create_sql_tool` | 将 SQL 创建为可复用工具(底层积木,需技能已存在) | 中等 |
|
||||
| `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 +149,11 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
|
||||
### 环境参数 `target`
|
||||
|
||||
- `prod` = 生产环境(正式数据,默认值)
|
||||
- `test` = 测试环境(测试数据)
|
||||
- `prod` = 生产环境(正式数据)
|
||||
- `test` = 测试环境(调试数据)
|
||||
|
||||
> **重要**:表数据 CRUD(query/insert/update/delete/export)的 `target` **默认 `test`**(安全优先)。要操作正式数据必须**显式传 `target="prod"`**。
|
||||
> 例外:`execute_sql` 的 target 默认 `prod`(沿用原行为)。
|
||||
|
||||
### 主键 `primaryKey`
|
||||
|
||||
@@ -120,20 +169,37 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
|
||||
## ⚠️ 安全确认原则(必须遵守)
|
||||
|
||||
### 〇、目标资源不得擅自选择(最高优先级铁律)
|
||||
|
||||
定位操作目标的参数(`datasourceId` / `connectionId` / `databaseName` / `tableId` / `tableName` / `apiKeyId` / `skillId` 等),**永远不得猜测、编造或"随手挑一个"**:
|
||||
|
||||
- **多个候选** → 列出全部,让用户选;严禁默认第一个或自认为"最合理"的那个。
|
||||
- **唯一候选** → 先声明"我将使用 XXX",给用户否决机会;创建/写入/删除类仍需用户确认后执行。
|
||||
- **没有候选 / 信息不足** → 直接问用户,不得编造。
|
||||
|
||||
> 当你发现自己正在"替用户决定用哪个数据源/数据库/表"时,立刻停下来改成提问。
|
||||
|
||||
**创建类操作**(`create_table` / `create_datasource` 等)执行前必须逐项确认落点,绝不"随便找个数据源就建"。例如建表须确认:数据源(`connectionId`)+ 数据库(`databaseName`)+ 表名(`tableName`),三者都要用户点头。
|
||||
|
||||
---
|
||||
|
||||
### 一、执行工具前的风险评估
|
||||
|
||||
当执行以下类型的操作时,**必须先询问用户确认**,不得擅作主张:
|
||||
|
||||
1. **删除操作**:删除数据源、删除表数据、删除 API 密钥、删除技能工具等
|
||||
1. **数据写操作(增 / 删 / 改)**:插入数据(`insert_table_row`)、更新数据(`update_table_row`)、删除数据(`delete_table_rows`)、导入数据(`confirm_import_data`)等
|
||||
- 说明:写操作会改变库中数据,**执行前必须把将要写入/修改/删除的具体内容预览给用户,等待用户明确确认后才执行**;删除不可恢复
|
||||
|
||||
2. **删除操作**:删除数据源、删除表数据、删除 API 密钥、删除技能工具等
|
||||
- 说明:此操作不可恢复,数据将永久丢失
|
||||
|
||||
2. **泄密风险操作**:导出包含敏感数据的表、创建 API 密钥、查看密钥详情等
|
||||
3. **泄密风险操作**:导出包含敏感数据的表、创建 API 密钥、查看密钥详情等
|
||||
- 说明:可能导致敏感信息泄露,需确认用户授权
|
||||
|
||||
3. **政治敏感操作**:涉及政治相关数据的查询、修改、删除等
|
||||
4. **政治敏感操作**:涉及政治相关数据的查询、修改、删除等
|
||||
- 说明:可能涉及合规风险,需确认用户意图
|
||||
|
||||
4. **疑似违规内容**:涉及色情、暴力、违法等内容的操作
|
||||
5. **疑似违规内容**:涉及色情、暴力、违法等内容的操作
|
||||
- 说明:可能违反法律法规,必须拒绝执行并告知用户
|
||||
|
||||
#### 确认格式
|
||||
@@ -294,26 +360,36 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
**用户**: "查一下 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": [{"id": 1, "username": "admin", "email": "admin@test.com"}, ...],
|
||||
"total": 156
|
||||
"data": {
|
||||
"tables": {
|
||||
"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 条:
|
||||
| id | username | email | created_at |
|
||||
|----|----------|-----------------|---------------------|
|
||||
| 1 | admin | admin@test.com | 2024-01-15 10:30:00 |
|
||||
| 2 | user1 | user1@test.com | 2024-01-16 14:20:00 |
|
||||
| id | username | email |
|
||||
|----|----------|-----------------|
|
||||
| 1 | admin | admin@test.com |
|
||||
| 2 | user1 | user1@test.com |
|
||||
...
|
||||
```
|
||||
|
||||
> **返回结构注意**:列定义在 `data.tables.columns[]`(字段名 `columnName`、是否主键 `isPrimaryKey`),行数据在 `data.content[]` 且是**位置数组**(按列顺序,不是键值对象)。展示/取主键时要把 content 的每个位置对到 columns 的同序字段。
|
||||
|
||||
### 注意事项
|
||||
- `target` 参数:`prod`=生产环境(默认),`test`=测试环境
|
||||
- `target` 参数:`test`=测试环境(**默认**),`prod`=生产环境(查正式数据须显式传)
|
||||
- 如果用户未指定数量,默认 `pageSize=10`
|
||||
- 如果用户说"翻页",增加 `pageNum` 参数
|
||||
- 如果用户想看更多数据,可以增大 `pageSize`(最大根据 API 限制)
|
||||
- 数据返回格式为对象数组
|
||||
- 行数据是位置数组(`data.content[]`),需对照 `data.tables.columns[]` 的列顺序解析
|
||||
|
||||
---
|
||||
|
||||
@@ -329,7 +405,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
1. 如果用户直接提供 SQL,直接使用
|
||||
如果用户提供自然语言需求,先转换为 SQL
|
||||
↓
|
||||
2. 调用 execute_sql(datasourceId="xx", executableSql="SELECT ...")
|
||||
2. 调用 execute_sql(datasourceId="xx", sql="SELECT ...")
|
||||
↓
|
||||
3. 展示查询结果
|
||||
↓
|
||||
@@ -345,7 +421,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
|
||||
调用: execute_sql(
|
||||
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"}],
|
||||
@@ -362,7 +438,8 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
### 注意事项
|
||||
- `datasourceId` 必须提供,如果用户未指定,先询问
|
||||
- `sqlTemplate` 可选,用于模板化查询
|
||||
- `parameters` 可选,用于参数化查询(防止 SQL 注入)
|
||||
- `params` 可选(对象),用于参数化查询(防止 SQL 注入);工具内部映射为后端的 `parameters`
|
||||
- `target` 可选:`test`=测试环境(默认),`prod`=生产环境
|
||||
- **执行危险操作(DELETE/DROP/TRUNCATE)前,必须向用户确认**
|
||||
- 如果查询结果超过 100 行,建议用户使用 `query_table_data` 代替
|
||||
- 对于只读查询(SELECT),可以直接执行;对于写操作(INSERT/UPDATE/DELETE),必须二次确认
|
||||
@@ -373,6 +450,8 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
|
||||
当用户需要修改表中的数据时,使用此流程。
|
||||
|
||||
> 🔒 **写操作统一规则(插入 / 更新 / 删除都适用)**:在调用 `insert_table_row` / `update_table_row` / `delete_table_rows` 之前,**必须先把将要写入/修改/删除的具体数据预览给用户,并等待用户明确确认后才执行**。不得在用户未确认的情况下直接落库。
|
||||
|
||||
### 4.1 插入数据
|
||||
|
||||
```
|
||||
@@ -382,9 +461,11 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
↓
|
||||
2. 确认必填字段(非空字段、无默认值字段)
|
||||
↓
|
||||
3. 调用 insert_table_row(tableId="xx", data={...})
|
||||
3. 向用户展示将要插入的数据,询问"确认插入以上数据?",等待用户确认
|
||||
↓
|
||||
4. 确认插入成功
|
||||
4. 用户确认后,调用 insert_table_row(tableId="xx", data={...})
|
||||
↓
|
||||
5. 确认插入成功
|
||||
```
|
||||
|
||||
**示例**:
|
||||
@@ -395,6 +476,12 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
调用: get_table_detail(tableId="5")
|
||||
返回: {columns: [{name: "id", isPrimaryKey: true, isAutoIncrement: true}, {name: "username", isNullable: false}, ...]}
|
||||
|
||||
回复: 即将向 users 表插入以下数据:
|
||||
username = test_user, email = test@test.com
|
||||
(id 自动生成)。确认插入吗?
|
||||
|
||||
用户: "确认"
|
||||
|
||||
调用: insert_table_row(
|
||||
tableId="5",
|
||||
data={"username": "test_user", "email": "test@test.com"}
|
||||
@@ -409,9 +496,11 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
↓
|
||||
1. 确认主键字段和要更新的值
|
||||
↓
|
||||
2. 调用 update_table_row(tableId="xx", primaryKey={主键}, data={要更新的字段})
|
||||
2. 向用户展示「目标行 + 改动前→改动后」的预览,询问"确认更新?",等待用户确认
|
||||
↓
|
||||
3. 确认更新成功
|
||||
3. 用户确认后,调用 update_table_row(tableId="xx", primaryKey={主键}, data={要更新的字段})
|
||||
↓
|
||||
4. 确认更新成功
|
||||
```
|
||||
|
||||
**示例**:
|
||||
@@ -419,6 +508,12 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
**用户**: "把 ID 为 5 的用户邮箱改成 new@test.com"
|
||||
|
||||
```
|
||||
回复: 即将更新 users 表 ID=5 的记录:
|
||||
email: old@test.com → new@test.com
|
||||
确认更新吗?
|
||||
|
||||
用户: "确认"
|
||||
|
||||
调用: update_table_row(
|
||||
tableId="5",
|
||||
primaryKey={"id": 5},
|
||||
@@ -491,13 +586,13 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
- **删除操作必须二次确认**
|
||||
- **写操作(插入/更新/删除)执行前必须先预览数据并等待用户确认**;删除操作不可恢复,需格外谨慎
|
||||
- `primaryKey` 必须是对象格式,如 `{"id": 1}`
|
||||
- `primaryKeys` 是数组格式,如 `[{"id": 1}, {"id": 2}]`
|
||||
- `data` 只包含要更新的字段,不需要提供全部字段
|
||||
- 插入数据时,自增主键不需要提供
|
||||
- 如果操作涉及多行,使用批量操作或循环调用
|
||||
- 增删改操作默认作用于 `prod` 环境
|
||||
- 增删改操作 `target` **默认 `test`**(安全优先);要写正式数据必须显式传 `target="prod"`
|
||||
|
||||
---
|
||||
|
||||
@@ -511,12 +606,20 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
用户请求: "我需要一个用户表" / "帮我设计一个订单系统的表结构"
|
||||
↓
|
||||
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 +630,34 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
调用: generate_table_by_description(
|
||||
requirement="我需要一个商品表,包含商品名称、价格、库存、分类,用于电商系统"
|
||||
)
|
||||
返回: {
|
||||
"tableName": "products",
|
||||
"tableComment": "商品表",
|
||||
"columns": [
|
||||
{"columnName": "id", "columnType": "SERIAL", "isPrimaryKey": true, "isAutoIncrement": true, "columnComment": "主键ID"},
|
||||
{"columnName": "name", "columnType": "VARCHAR", "columnLength": 200, "isNullable": false, "columnComment": "商品名称"},
|
||||
{"columnName": "price", "columnType": "DECIMAL", "columnLength": 10, "columnComment": "价格"},
|
||||
{"columnName": "stock", "columnType": "INTEGER", "columnComment": "库存数量"},
|
||||
{"columnName": "category_id", "columnType": "INTEGER", "columnComment": "分类ID"},
|
||||
{"columnName": "created_at", "columnType": "TIMESTAMP", "defaultValue": "NOW()", "columnComment": "创建时间"}
|
||||
]
|
||||
返回: {"data": {"taskId": "831", "status": "0", "message": "任务已提交,等待AI生成表结构"}}
|
||||
← 异步!返回的是 taskId,不是表结构
|
||||
|
||||
轮询: get_ai_training_detail(taskId="831")
|
||||
返回(完成时): {
|
||||
"data": {
|
||||
"trainingStatus": "2", // 2=已完成
|
||||
"progress": 100,
|
||||
"createTableData": {"data": {"tables": [{
|
||||
"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 已生成表结构设计:
|
||||
表名: products (商品表)
|
||||
字段:
|
||||
- id (SERIAL, 主键, 自增) - 主键ID
|
||||
- id (SERIAL, 主键) - 主键ID
|
||||
- name (VARCHAR(200), 非空) - 商品名称
|
||||
- price (DECIMAL(10)) - 价格
|
||||
- stock (INTEGER) - 库存数量
|
||||
- category_id (INTEGER) - 分类ID
|
||||
- created_at (TIMESTAMP, 默认NOW()) - 创建时间
|
||||
|
||||
是否需要在此基础上修改或直接创建?
|
||||
|
||||
@@ -559,7 +668,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
databaseName="order_db",
|
||||
tableName="products",
|
||||
tableComment="商品表",
|
||||
columns=[...] // 使用 AI 生成的 columns
|
||||
columns=[...] // 使用轮询拿到的 createTableData.data.tables[0].columns
|
||||
)
|
||||
回复: 已成功创建表 products (商品表)
|
||||
```
|
||||
@@ -580,15 +689,13 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
4. 确认修改成功
|
||||
```
|
||||
|
||||
**可用的 operations 类型**:
|
||||
**可用的 operations 类型(真机验证,后端仅支持这 3 种)**:
|
||||
- `ADD_COLUMN`:添加字段
|
||||
- `MODIFY_COLUMN`:修改字段(改类型/长度/可空/默认值/注释,整列重定义)
|
||||
- `DROP_COLUMN`:删除字段
|
||||
- `RENAME_COLUMN`:重命名字段
|
||||
- `ALTER_COLUMN_TYPE`:修改字段类型
|
||||
- `SET_NOT_NULL`:设置为非空
|
||||
- `DROP_NOT_NULL`:取消非空约束
|
||||
- `SET_DEFAULT`:设置默认值
|
||||
- `DROP_DEFAULT`:删除默认值
|
||||
|
||||
> ⚠️ 后端**不支持** `RENAME_COLUMN`/`ALTER_COLUMN_TYPE`/`SET_NOT_NULL`/`DROP_NOT_NULL`/`SET_DEFAULT`/`DROP_DEFAULT`(会报「不支持的操作类型」)。改字段类型/约束统一用 `MODIFY_COLUMN` 传完整列定义。
|
||||
> `operations[].column` 是列定义对象,字段同建表(columnName/columnType/columnLength/isNullable/columnComment/defaultValue 等)。
|
||||
|
||||
### 注意事项
|
||||
- `requirement` 参数应尽可能详细,包含业务场景和字段需求
|
||||
@@ -608,9 +715,9 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
```
|
||||
用户请求: "帮我导入这个 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 识别的表结构和数据预览
|
||||
↓
|
||||
@@ -618,17 +725,63 @@ 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 插入行数)
|
||||
```
|
||||
|
||||
### confirm_import_data 的 data 传什么
|
||||
|
||||
把 `preview_import_data` 返回的 data 原文整块传给 `data` 参数即可,工具会自动解包组装。data 的标准形态(= preview 返回):
|
||||
|
||||
```json
|
||||
{
|
||||
"tableStructure": {
|
||||
"success": true,
|
||||
"message": "Excel表结构生成成功",
|
||||
"data": {
|
||||
"tables": [
|
||||
{
|
||||
"tableName": "animals",
|
||||
"tableComment": "宠物信息表",
|
||||
"columns": [
|
||||
{ "columnName": "id", "columnType": "SERIAL", "isPrimaryKey": true, "isAdditionField": true },
|
||||
{ "columnName": "animal_name", "columnType": "VARCHAR", "columnLength": 5000 }
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"allData": [
|
||||
["id", "animal_name", "..."],
|
||||
["1", "豆豆", "..."],
|
||||
["2", "咪咪", "..."]
|
||||
]
|
||||
},
|
||||
"databaseName": "pp_test",
|
||||
"target": "prod"
|
||||
}
|
||||
```
|
||||
|
||||
- `tableStructure`:preview 的表结构包装(`{success, message, data:{tables:[...]}}`),工具会取 `tables[0]` 当单表对象,并把 `databaseName` 塞进去。
|
||||
- `databaseName` / `target` 既可放顶层参数,也可放在 `data` 里,工具都认。
|
||||
- ⚠️ **allData 是二维数组,首行必须是「列名表头行」**:
|
||||
- `allData[0]` = 各列的 `columnName`(列名表头),**真实数据从 `allData[1]` 起**;
|
||||
- 每行(含表头行)都是按 `columns` 顺序排列的位置数组,**行宽 = 列总数**(含 `SERIAL` 主键等所有列,**不裁剪**,主键列给占位值即可,后端自增时会忽略);
|
||||
- 若传入的 allData 没带表头行(首行就是数据),工具会**据列名自动补一行表头**——否则后端会把首行数据当成字段名,报「查询字段不存在/字段名称不正确」。
|
||||
- ⚠️ **columns / 表头列名必须对应目标表真实存在的字段**:后端按列名拼 INSERT,`tableStructure.columns`(以及 allData 首行表头)里的列名必须是目标表里**确实存在的字段名**,否则报「查询字段不存在 / 字段名称不正确」。
|
||||
- **导入到已有表时**:不要直接用 Excel 识别出的列,应**先 `get_table_detail(tableId="xx")` 拿到目标表真实字段,再把 data 里的 columns、allData 首行表头、各行取值都对齐到这些真实字段**(前端就是用目标表真实列覆盖 AI 识别列的)。
|
||||
|
||||
### 注意事项
|
||||
- 文件大小限制:< 500KB
|
||||
- 支持格式:.xlsx / .xls
|
||||
- 导入前默认使用 `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 字段」,此时需按预览返回的列名核对
|
||||
- 后端报「插入数据失败(第N行):查询字段不存在/字段名称不正确」时,多半是 **allData 缺了列名表头行**(后端把首行数据当成了字段名),或某行列数与 `columns` 对不上——确认首行是列名、每行值个数 = 列总数
|
||||
|
||||
---
|
||||
|
||||
@@ -707,11 +860,55 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
- `database`:数据库级别权限
|
||||
- `table`:表级别权限
|
||||
|
||||
> ⚠️ 权限为「仅追加」模型:`grant_api_key_permissions` 只能新增权限,不会覆盖已有权限,且后端**不支持撤销/删除已授予的权限**。授权前务必确认范围,授错只能删掉整个密钥(`delete_api_key`)后重建。
|
||||
|
||||
### 7.7 调整权限(只能重建密钥)
|
||||
|
||||
```
|
||||
# 后端不支持撤销单条权限。如需收回某密钥的权限,只能删除密钥后重新创建并重新授权:
|
||||
调用: delete_api_key(id="7")
|
||||
调用: create_api_key(apiKeyName="xxx")
|
||||
调用: grant_api_key_permissions(apiKeyId="<新密钥ID>", batchDatas=[...])
|
||||
```
|
||||
|
||||
> 说明:后端 permission 删除接口返回「不支持当前的调用方式」,无法撤销单条已授予权限。要缩小权限范围,走「删密钥 → 重建 → 重新授权」。
|
||||
|
||||
---
|
||||
|
||||
## 场景 8:技能与工具管理
|
||||
|
||||
当用户需要创建和管理自定义技能时,使用此流程。
|
||||
当用户需要把 SQL 沉淀为数据源的可复用工具时,使用此流程。
|
||||
|
||||
> 🔒 **核心约束:技能(skill)必须挂着工具才有效**。后端/前端都没有「只建空技能」这个动作——单独建技能会留下一个无效的空技能。前端唯一入口是「添加工具」,它会按需把技能建好、配好,最后必定以创建工具收尾。
|
||||
|
||||
### 8.0 一步到位:把 SQL 沉淀为工具(推荐)
|
||||
|
||||
**优先用 `add_sql_tool_to_datasource`**,它一步完成整条链路,保证技能必有工具:
|
||||
|
||||
```
|
||||
调用: add_sql_tool_to_datasource(
|
||||
datasourceId="58",
|
||||
name="查询活跃用户",
|
||||
businessDescription="查询所有状态为活跃的用户",
|
||||
sqlTemplate="SELECT * FROM users WHERE status = #{status}",
|
||||
sqlParams={"type":"object","required":["status"],"properties":{"status":{"type":"string","description":"用户状态","examples":["active"]}}}, // 可选
|
||||
resultType="list", // 可选,默认 list
|
||||
businessScenario="用于查看当前活跃用户列表", // 可选
|
||||
tableIds=["5"] // 可选
|
||||
)
|
||||
```
|
||||
|
||||
> **sqlParams 格式**:标准 JSON Schema,形如 `{"type":"object","required":[...],"properties":{"参数名":{"type":"...","description":"...","examples":[...]}}}`。`properties` 的键要与 SQL 模板里的 `#{参数名}` 占位符一一对应。传 dict 会自动序列化为 JSON 字符串;无参数可传 `{}`。
|
||||
|
||||
内部流程(= 前端 handleAddToolSubmit):
|
||||
```
|
||||
读 skillBool(GET /datasource/config/{id})
|
||||
├─ 技能已存在 → getByDatasource 拿 skillId → 按 sqlTemplate 去重 → confirmTools 建工具
|
||||
└─ 技能不存在 → createOrGet 建技能 → getByDatasource 拿 skillId → 去重
|
||||
→ updateOrGet 写 lzwcai-mcp-sqlexecutor 配置模板 → confirmTools 建工具
|
||||
```
|
||||
- **幂等**:同 datasourceId 下若已有 `sqlTemplate` 相同的工具(空白归一化后比较),返回 `skipped`,不重复创建。
|
||||
- **skillId 来自 getByDatasource**,不是 createOrGet 的返回(后端 createOrGet 不回可靠 id)。
|
||||
|
||||
### 8.1 查看数据源关联的技能
|
||||
|
||||
@@ -719,21 +916,23 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
调用: get_skill_by_datasource(datasourceId="58")
|
||||
```
|
||||
|
||||
### 8.2 创建技能
|
||||
|
||||
```
|
||||
调用: create_skill(datasourceId="58", name="订单查询技能", description="用于订单数据的常用查询")
|
||||
```
|
||||
|
||||
### 8.3 查看技能下的工具
|
||||
|
||||
```
|
||||
调用: get_skill_tools(skillId="xx")
|
||||
```
|
||||
|
||||
### 8.4 将 SQL 创建为可复用工具
|
||||
---
|
||||
|
||||
### 分步操作(高级,一般不需要)
|
||||
|
||||
> ⚠️ **不要单独建空技能**:技能必须挂着工具才有效,平时不会单独创建技能。常规「把 SQL 沉淀为工具」一律用 `add_sql_tool_to_datasource`(它会按需建技能+配模板+建工具)。下面的散工具仅用于「技能已存在」时单独加/改工具。
|
||||
|
||||
### 8.4 向已有技能添加 SQL 工具(底层积木,需技能已存在)
|
||||
|
||||
```
|
||||
# 前提:技能已存在(用 get_skill_by_datasource 拿到 skillId);
|
||||
# 若技能不存在,请直接用 add_sql_tool_to_datasource,别手动建空技能
|
||||
调用: create_sql_tool(
|
||||
skillId="xx",
|
||||
tableIds=["5"],
|
||||
@@ -741,7 +940,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
"name": "查询活跃用户",
|
||||
"businessDescription": "查询所有状态为活跃的用户",
|
||||
"sqlTemplate": "SELECT * FROM users WHERE status = #{status}",
|
||||
"sqlParams": {"status": {"type": "string", "default": "active"}},
|
||||
"sqlParams": {"type":"object","required":["status"],"properties":{"status":{"type":"string","description":"用户状态","examples":["active"]}}},
|
||||
"resultType": "list",
|
||||
"businessScenario": "用于查看当前活跃用户列表"
|
||||
}]
|
||||
@@ -762,12 +961,38 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
### 8.6 更新技能配置
|
||||
|
||||
```
|
||||
# datasourceId + skillId 均必填(真实 ID,来自其他工具返回,不可臆造)
|
||||
# 只给这两个 ID 即自动生成 lzwcai-mcp-sqlexecutor 标准配置模板
|
||||
调用: update_skill_config(
|
||||
datasourceId="58", // 来自 list_databases / list_tables_with_ai / get_connection_config_list
|
||||
skillId="xx" // 来自 get_skill_by_datasource 返回
|
||||
)
|
||||
|
||||
# 或:手动指定完整 configTemplate(覆盖自动生成)
|
||||
调用: update_skill_config(
|
||||
datasourceId="58",
|
||||
configTemplate='{"mcpServer": "..."}' // JSON 字符串
|
||||
skillId="xx",
|
||||
configTemplate='{"mcpServers": {...}}' // JSON 字符串
|
||||
)
|
||||
```
|
||||
|
||||
> 说明:configTemplate 大部分是固定值,只有 mcpServerKey 后缀、`env.databaseId`、`env.skillId` 随 datasourceId / skillId 变化。两个 ID 都必填且必须是其他工具返回的真实值。不显式传 configTemplate 时,工具会按标准模板自动生成(与前端 SqlControllerMsg.vue 一致),无需手写整段 JSON。
|
||||
|
||||
|
||||
### 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:表订阅管理
|
||||
@@ -778,13 +1003,117 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
调用: toggle_table_subscription(
|
||||
configId="数据库配置ID",
|
||||
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. 安全第一
|
||||
|
||||
@@ -839,8 +1168,8 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
- `1` - 已停止
|
||||
|
||||
### 环境
|
||||
- `prod` - 生产环境(默认)
|
||||
- `test` - 测试环境
|
||||
- `test` - 测试环境(**表数据 CRUD 的默认值**,安全优先)
|
||||
- `prod` - 生产环境(操作正式数据须显式传 `target="prod"`;仅 `execute_sql` 默认 prod)
|
||||
|
||||
### 常用字段类型
|
||||
| 类型 | 用途 | 示例 |
|
||||
@@ -854,16 +1183,13 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
| `BOOLEAN` | 布尔值 | `is_active BOOLEAN` |
|
||||
|
||||
### 表结构变更操作类型(alter_table)
|
||||
| 类型 | 用途 |
|
||||
|------|------|
|
||||
| `ADD_COLUMN` | 添加字段 |
|
||||
| `DROP_COLUMN` | 删除字段 |
|
||||
| `RENAME_COLUMN` | 重命名字段 |
|
||||
| `ALTER_COLUMN_TYPE` | 修改字段类型 |
|
||||
| `SET_NOT_NULL` | 设置为非空 |
|
||||
| `DROP_NOT_NULL` | 取消非空约束 |
|
||||
| `SET_DEFAULT` | 设置默认值 |
|
||||
| `DROP_DEFAULT` | 删除默认值 |
|
||||
| 类型 | 用途 | column 对象关键字段 |
|
||||
|------|------|------|
|
||||
| `ADD_COLUMN` | 添加字段 | columnName, columnType, columnLength, isNullable, columnComment |
|
||||
| `MODIFY_COLUMN` | 修改字段(类型/长度/约束等,含改类型) | columnName, columnType, columnLength, ... |
|
||||
| `DROP_COLUMN` | 删除字段 | columnName |
|
||||
|
||||
> 后端**只支持上述 3 种**。`RENAME_COLUMN`/`ALTER_COLUMN_TYPE`/`SET_NOT_NULL`/`DROP_NOT_NULL`/`SET_DEFAULT`/`DROP_DEFAULT` 均会报「不支持的操作类型」(已真机验证)。改字段类型用 `MODIFY_COLUMN`。
|
||||
|
||||
### 权限级别(API 密钥授权)
|
||||
| 级别 | 范围 |
|
||||
@@ -879,7 +1205,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据
|
||||
如果用户说"帮我查一下数据库",按以下步骤操作:
|
||||
|
||||
1. 调用 `list_datasources()` 获取数据源列表
|
||||
2. 展示列表,让用户选择或默认第一个运行中的数据源
|
||||
2. 展示列表,让用户选择目标数据源(**多个候选时必须让用户选,不得默认第一个**;只有一个候选也要先说明"我将使用 XXX"再继续)
|
||||
3. 调用 `get_datasource_detail(datasourceId="xx")` 获取数据库和表信息
|
||||
4. 引导用户选择要操作的表
|
||||
5. 根据用户意图调用相应的工具:
|
||||
|
||||
Binary file not shown.
343
.kilo/skills/数字员工平台数据库技能的.md
Normal file
343
.kilo/skills/数字员工平台数据库技能的.md
Normal file
@@ -0,0 +1,343 @@
|
||||
# 数字员工平台数据库技能配置指南
|
||||
|
||||
## 概述
|
||||
|
||||
本文档介绍两种使用数字员工平台数据库技能的方法:
|
||||
- **方法一**:通过数字员工对话直接使用
|
||||
- **方法二**:通过 AI 编辑器的 Skills + MCP 搭配使用
|
||||
|
||||
---
|
||||
|
||||
## 方法一:通过数字员工对话使用
|
||||
|
||||
### 配置说明
|
||||
|
||||
此方法通过配置 MCP Server 来连接数字员工平台。
|
||||
|
||||
### MCP 配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"lzwcai-mcp-agile-db": {
|
||||
"command": "uvx",
|
||||
"type": "stdio",
|
||||
"args": [
|
||||
"lzwcai-mcp-agile-db"
|
||||
],
|
||||
"timeout": 600,
|
||||
"env": {
|
||||
"API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 注意事项
|
||||
|
||||
- 配置中的 `API_KEY` 是数字员工平台的密钥
|
||||
- 存在单点登录问题,请使用不会过期的账号密钥
|
||||
- Skills 配置参考:`lzwcai-agile-db`
|
||||
|
||||
---
|
||||
|
||||
## 方法二:通过 AI 编辑器使用
|
||||
|
||||
### 配置说明
|
||||
|
||||
此方法通过下载 Skills 和 MCP 配置,在 AI 编辑器中搭配使用,支持直接对话操作。
|
||||
|
||||
### MCP 配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"lzwcai_mcp_agile_db": {
|
||||
"command": "uvx",
|
||||
"args": [
|
||||
"lzwcai-mcp-agile-db"
|
||||
],
|
||||
"env": {
|
||||
"UV_INDEX_URL": "http://192.168.2.236:3141/lzwc/dev/+simple/",
|
||||
"API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q",
|
||||
"backendBaseUrl": "http://192.168.2.236:8088"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 环境变量说明
|
||||
|
||||
| 变量名 | 说明 | 示例值 |
|
||||
|--------|------|--------|
|
||||
| `UV_INDEX_URL` | Python 包索引地址 | `http://192.168.2.236:3141/lzwc/dev/+simple/` |
|
||||
| `API_KEY` | 数字员工平台 API 密钥 | (见配置示例) |
|
||||
| `backendBaseUrl` | 后端服务基础地址 | `http://192.168.2.236:8088` |
|
||||
|
||||
---
|
||||
|
||||
## 使用建议
|
||||
|
||||
1. **选择方法**:根据实际使用场景选择合适的方法
|
||||
- 数字员工对话:适合快速使用
|
||||
- AI 编辑器:适合更灵活的配置和使用
|
||||
|
||||
2. **密钥管理**:妥善保管 `API_KEY`,避免泄露
|
||||
|
||||
3. **网络配置**:确保网络可以访问配置的后端地址
|
||||
|
||||
---
|
||||
|
||||
## 相关资源
|
||||
|
||||
- Skills 目录:`lzwcai-agile-db/`
|
||||
- 详细技能文档:`lzwcai-agile-db/SKILL.md`
|
||||
|
||||
---
|
||||
|
||||
## 对话使用场景示例
|
||||
|
||||
以下是配置好技能后,您可以直接使用的对话示例:
|
||||
|
||||
### 📊 场景 1:浏览数据源和表
|
||||
|
||||
**用户:** "帮我看看有哪些数据源"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
共找到 3 个数据源:
|
||||
1. HMD产品(PostgreSQL, 运行中)
|
||||
2. 测试数据库(MySQL, 运行中)
|
||||
3. 历史数据(Oracle, 已停止)
|
||||
|
||||
请告诉我您想看哪个数据源?
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**用户:** "看看HMD产品有哪些表"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
HMD产品 数据源包含以下数据库和表:
|
||||
|
||||
order_db:
|
||||
- orders (订单表, 15 个字段, 2568 条记录)
|
||||
- users (用户表, 8 个字段, 156 条记录)
|
||||
- products (商品表, 12 个字段, 892 条记录)
|
||||
|
||||
analytics_db:
|
||||
- daily_stats (日报表, 10 个字段, 365 条记录)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔍 场景 2:查询表数据
|
||||
|
||||
**用户:** "查一下users表前10条数据"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
users 表共 156 条记录,当前显示第 1-10 条:
|
||||
|
||||
| id | username | email | status | created_at |
|
||||
|----|----------|-----------------|--------|---------------------|
|
||||
| 1 | admin | admin@test.com | active | 2024-01-01 00:00:00 |
|
||||
| 2 | user1 | user1@test.com | active | 2024-01-02 10:30:00 |
|
||||
| 3 | user2 | user2@test.com | inactive| 2024-01-03 15:45:00 |
|
||||
...
|
||||
|
||||
注:默认查询测试环境,如需查询生产环境请说明。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📝 场景 3:执行 SQL 查询
|
||||
|
||||
**用户:** "统计每个地区的订单数量"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
各地区订单统计:
|
||||
|
||||
| 地区 | 订单数 |
|
||||
|------|--------|
|
||||
| 华东 | 1,250 |
|
||||
| 华南 | 980 |
|
||||
| 华北 | 756 |
|
||||
| 西南 | 432 |
|
||||
| 东北 | 289 |
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ➕ 场景 4:插入数据
|
||||
|
||||
**用户:** "新增一个用户,用户名是test_user,邮箱test@test.com"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
即将向 users 表插入以下数据:
|
||||
username = test_user
|
||||
email = test@test.com
|
||||
(id 自动生成)
|
||||
|
||||
确认插入吗?
|
||||
```
|
||||
|
||||
**用户:** "确认"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已成功插入用户 test_user (ID: 157)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### ✏️ 场景 5:更新数据
|
||||
|
||||
**用户:** "把ID为5的用户邮箱改成new@test.com"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
即将更新 users 表 ID=5 的记录:
|
||||
email: old@test.com → new@test.com
|
||||
|
||||
确认更新吗?
|
||||
```
|
||||
|
||||
**用户:** "确认"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已更新用户 ID=5 的邮箱为 new@test.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🗑️ 场景 6:删除数据
|
||||
|
||||
**用户:** "删除ID为10的订单"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
⚠️ 安全提醒:此操作存在删除风险。
|
||||
具体说明:数据将永久删除,不可恢复。
|
||||
|
||||
确认要删除以下记录吗?
|
||||
订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00
|
||||
|
||||
请回复"确认删除"继续。
|
||||
```
|
||||
|
||||
**用户:** "确认删除"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已成功删除订单 ID=10
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🤖 场景 7:AI 生成表结构
|
||||
|
||||
**用户:** "帮我设计一个商品表,包含名称、价格、库存、分类"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
AI 已生成表结构设计:
|
||||
|
||||
表名: products (商品表)
|
||||
字段:
|
||||
- id (SERIAL, 主键) - 主键ID
|
||||
- name (VARCHAR(200), 非空) - 商品名称
|
||||
- price (DECIMAL(10,2)) - 价格
|
||||
- stock (INTEGER) - 库存数量
|
||||
- category (VARCHAR(50)) - 分类
|
||||
- created_at (TIMESTAMP) - 创建时间
|
||||
|
||||
是否需要在此基础上修改或直接创建?
|
||||
```
|
||||
|
||||
**用户:** "直接创建到order_db数据库"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已成功创建表 products (商品表)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 📥 场景 8:导入 Excel 数据
|
||||
|
||||
**用户:** "帮我导入这个Excel文件:https://example.com/data.xlsx"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
AI 已识别 Excel 文件内容:
|
||||
|
||||
表名: customers (客户表)
|
||||
字段:
|
||||
- id (SERIAL, 主键)
|
||||
- name (VARCHAR(100)) - 客户名称
|
||||
- phone (VARCHAR(20)) - 电话
|
||||
- address (TEXT) - 地址
|
||||
|
||||
预览数据(前3条):
|
||||
| id | name | phone | address |
|
||||
|----|--------|------------|----------------|
|
||||
| 1 | 张三 | 13800138000| 北京市朝阳区 |
|
||||
| 2 | 李四 | 13900139000| 上海市浦东新区 |
|
||||
| 3 | 王五 | 13700137000| 广州市天河区 |
|
||||
|
||||
共 50 条数据待导入。
|
||||
|
||||
⚠️ 安全提醒:此操作可能涉及数据安全风险。
|
||||
说明:导入的数据将写入数据库,请确认数据来源合法合规。
|
||||
请确认是否继续?
|
||||
```
|
||||
|
||||
**用户:** "继续"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 成功导入 50 条数据到 customers 表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 🔑 场景 9:API 密钥管理
|
||||
|
||||
**用户:** "帮我创建一个新的API密钥"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
请输入密钥名称:
|
||||
```
|
||||
|
||||
**用户:** "测试密钥"
|
||||
|
||||
**AI 回复:**
|
||||
```
|
||||
✅ 已创建 API 密钥:
|
||||
名称: 测试密钥
|
||||
密钥: lzwc_sk_xxxxxxxxxxxx
|
||||
状态: 启用
|
||||
过期时间: 2025-06-18
|
||||
|
||||
请妥善保管密钥,丢失后无法找回。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 重要使用提示
|
||||
|
||||
1. **环境选择:** 默认使用测试环境(`test`),操作生产环境需要明确说明"查询生产环境"或"操作生产环境"
|
||||
|
||||
2. **安全确认:** 所有写操作(增删改)都会先预览并等待您的确认
|
||||
|
||||
3. **多选原则:** 有多个数据源/数据库/表可选时,AI会列出让您选择,不会擅自做主
|
||||
|
||||
4. **分步执行:** 复杂任务会分步完成,每一步都会确认后再继续
|
||||
@@ -1,6 +1,6 @@
|
||||
# lzwcai-mcp-agile-db
|
||||
|
||||
数据库管理平台 MCP Server,提供 33 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。
|
||||
数据库管理平台 MCP Server,提供 34 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。
|
||||
|
||||
## 环境变量
|
||||
|
||||
@@ -57,15 +57,16 @@ lzwcai-mcp-agile-db
|
||||
- `toggle_api_key_status` - 启用/禁用密钥
|
||||
- `delete_api_key` - 删除密钥
|
||||
- `get_api_key_permissions` - 查看密钥权限
|
||||
- `grant_api_key_permissions` - 授予权限
|
||||
- `grant_api_key_permissions` - 授予权限(仅追加,不可撤销)
|
||||
|
||||
### 技能与工具管理
|
||||
- `add_sql_tool_to_datasource` - 把 SQL 沉淀为工具(一步到位,自动建技能+配模板+建工具,推荐入口)
|
||||
- `get_skill_by_datasource` - 获取技能信息
|
||||
- `get_skill_tools` - 获取技能工具列表
|
||||
- `create_skill` - 创建技能
|
||||
- `create_sql_tool` - 创建 SQL 工具
|
||||
- `create_sql_tool` - 创建 SQL 工具(需技能已存在)
|
||||
- `delete_skill_tool` - 删除技能工具
|
||||
- `update_skill_config` - 更新技能配置
|
||||
- `update_skill_tool` - 修改技能工具
|
||||
|
||||
### 数据导入
|
||||
- `preview_import_data` - 预览导入数据
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,202 +1,377 @@
|
||||
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-11 09:30:49 - 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-11 09:30:49 - 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-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
|
||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
|
||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-11 09:30:49 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
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,
|
||||
2026-06-22 23:02:48 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-22 23:02:48 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:73] - [客户端初始化] base_url=https://dempdemo.lzwcai.com/api, 认证方式=account:demp04
|
||||
2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:118] - [登录] POST https://dempdemo.lzwcai.com/api/login, username=demp04, loginType=user
|
||||
2026-06-22 23:02:48 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12F4F5C0>
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000020F12F165D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12DFD970>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Mon, 22 Jun 2026 15:02:58 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
||||
2026-06-22 23:02:49 - httpx - INFO - [_client.py:1740] - HTTP Request: POST https://dempdemo.lzwcai.com/api/login "HTTP/1.1 200 "
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:161] - [API响应] HTTP 200
|
||||
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:137] - [登录] 成功获取 token
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 09:47:20 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
|
||||
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-23 09:47:20 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB167DF860>
|
||||
2026-06-23 09:47:21 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8088, 认证方式=account:yy8z9
|
||||
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
|
||||
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB156EB260>
|
||||
2026-06-23 09:47:28 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8088/login, username=yy8z9, loginType=user
|
||||
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001FB16D14260>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
|
||||
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8088/login "HTTP/1.1 200 "
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8088/datasource/api_key/list
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
|
||||
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8088/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 21,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-10 16:47:43",
|
||||
"createTime": "2026-06-18 11:29:46",
|
||||
"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",
|
||||
"updateTime": "2026-06-18 11:59:25",
|
||||
"remark": null,
|
||||
"id": "7",
|
||||
"apiKey": "Lb8LgEJ7eBUU8QMifKUJvo9w6YLAotbKJ-w1DKU8ZrU",
|
||||
"apiKeyName": "AWINBEXT",
|
||||
"enterpriseId": "1937166012193443842",
|
||||
"id": "37",
|
||||
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||
"apiKeyName": "盒马超市只读访问密钥",
|
||||
"enterpriseId": "1932095424144715777",
|
||||
"status": 0,
|
||||
"expireTime": "2027-06-06T15:10:32.000+08:00"
|
||||
"expireTime": "2027-06-18T11:59:25.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
|
||||
"createTime": "2026-06-18 10:54:40",
|
||||
...
|
||||
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 09:48:33 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
|
||||
2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-23 09:48:33 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001F89A81AAB0>
|
||||
2026-06-23 09:48:34 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
|
||||
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
|
||||
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001F89A273B00>
|
||||
2026-06-23 09:48:36 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
|
||||
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001F89C2B8F20>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 21,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 11:29:46",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-06-18 11:59:25",
|
||||
"remark": null,
|
||||
"id": "37",
|
||||
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||
"apiKeyName": "盒马超市只读访问密钥",
|
||||
"enterpriseId": "1932095424144715777",
|
||||
"status": 0,
|
||||
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 10:54:40",
|
||||
...
|
||||
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 11:11:10 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
|
||||
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-23 11:11:10 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCAFE606E0>
|
||||
2026-06-23 11:11:11 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
|
||||
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
|
||||
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0FBFB60>
|
||||
2026-06-23 11:11:14 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
|
||||
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB15974D0>
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 21,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 11:29:46",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-06-18 11:59:25",
|
||||
"remark": null,
|
||||
"id": "37",
|
||||
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||
"apiKeyName": "盒马超市只读访问密钥",
|
||||
"enterpriseId": "1932095424144715777",
|
||||
"status": 0,
|
||||
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 10:54:40",
|
||||
...
|
||||
2026-06-23 11:11:15 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0EA5D60>
|
||||
2026-06-23 11:11:27 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
|
||||
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB100F1D0>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=utf-8'), (b'Content-Length', b'51'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN')])
|
||||
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:239] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
|
||||
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
|
||||
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 21,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 11:29:46",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-06-18 11:59:25",
|
||||
"remark": null,
|
||||
"id": "37",
|
||||
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
|
||||
"apiKeyName": "盒马超市只读访问密钥",
|
||||
"enterpriseId": "1932095424144715777",
|
||||
"status": 0,
|
||||
"expireTime": "2027-06-18T11:59:25.000+08:00"
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-18 10:54:40",
|
||||
...
|
||||
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
|
||||
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
|
||||
2026-06-23 11:37:08 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-23 11:37:08 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
|
||||
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
|
||||
|
||||
@@ -1,15 +1 @@
|
||||
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-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
lzwcai-mcp-agile-db MCP Server
|
||||
数据库管理平台 MCP 工具服务,提供 33 个工具用于数据库管理、表操作、API 密钥管理等
|
||||
数据库管理平台 MCP 工具服务,提供数据库管理、表/数据操作、技能、API 密钥、MQTT 同步、AI 训练等工具
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -104,19 +104,24 @@ async def handle_call_tool(
|
||||
|
||||
async def run_server():
|
||||
"""运行 MCP Server (stdio 模式)"""
|
||||
async with stdio_server() as streams:
|
||||
await server.run(
|
||||
streams[0],
|
||||
streams[1],
|
||||
InitializationOptions(
|
||||
server_name="lzwcai_mcp_agile_db",
|
||||
server_version="0.1.0",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
try:
|
||||
async with stdio_server() as streams:
|
||||
await server.run(
|
||||
streams[0],
|
||||
streams[1],
|
||||
InitializationOptions(
|
||||
server_name="lzwcai_mcp_agile_db",
|
||||
server_version="0.1.8",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
finally:
|
||||
# 释放全局 HTTP 客户端
|
||||
if _api_client is not None:
|
||||
await _api_client.close()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
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,8 @@
|
||||
"""
|
||||
API 密钥管理工具 (工具 18-23)
|
||||
API 密钥管理工具(含创建、状态切换、删除、权限查询/授予)
|
||||
|
||||
注意:权限模型为「仅追加」——grant_api_key_permissions 只能新增权限,后端不支持撤销/删除
|
||||
已授予的权限(真机验证 permission 删除接口返回「不支持当前的调用方式」),故不提供 revoke 工具。
|
||||
"""
|
||||
|
||||
from ._base import register_tool, ToolDef
|
||||
@@ -93,7 +96,7 @@ class GetApiKeyPermissionsTool(ToolDef):
|
||||
@register_tool("grant_api_key_permissions")
|
||||
class GrantApiKeyPermissionsTool(ToolDef):
|
||||
name = "grant_api_key_permissions"
|
||||
description = "批量为 API 密钥授予权限"
|
||||
description = "批量为 API 密钥授予权限(仅追加,不会覆盖或删除已有权限;后端不支持撤销已授予的权限)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
|
||||
@@ -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)
|
||||
"""
|
||||
|
||||
import base64
|
||||
import io
|
||||
import mimetypes
|
||||
import os
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
from ._base import register_tool, ToolDef
|
||||
|
||||
@@ -11,31 +13,75 @@ from ._base import register_tool, ToolDef
|
||||
@register_tool("preview_import_data")
|
||||
class PreviewImportDataTool(ToolDef):
|
||||
name = "preview_import_data"
|
||||
description = "上传 Excel 文件,AI 智能识别并预览表结构/数据"
|
||||
description = "从 URL 下载 Excel 文件,AI 智能识别并预览表结构/数据"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源 ID"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"},
|
||||
"file_base64": {"type": "string", "description": "Excel 文件 base64 编码(.xlsx/.xls, <500KB)"},
|
||||
"file_name": {"type": "string", "description": "文件名(如 data.xlsx)"},
|
||||
"file_url": {"type": "string", "description": "Excel 文件下载地址(.xlsx/.xls, <500KB)"},
|
||||
},
|
||||
"required": ["connectionId", "file_base64"],
|
||||
"required": ["connectionId", "file_url"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
target = args.pop("target", "test")
|
||||
file_base64 = args.pop("file_base64")
|
||||
file_name = args.pop("file_name", "import.xlsx")
|
||||
file_url = args.pop("file_url")
|
||||
|
||||
# 解码 base64 文件
|
||||
file_content = base64.b64decode(file_base64)
|
||||
# 0. 基础 URL 校验
|
||||
parsed_url = urlparse(file_url)
|
||||
if parsed_url.scheme not in ("http", "https"):
|
||||
raise ValueError("仅支持 http/https 协议的下载地址")
|
||||
|
||||
# 构建文件上传
|
||||
# 1. 下载文件
|
||||
download_resp = await self.client.client.get(file_url, follow_redirects=True, timeout=30.0)
|
||||
download_resp.raise_for_status()
|
||||
|
||||
# 1.1 预先根据 Content-Length 拦截超大文件,避免无意义下载
|
||||
max_size = 500 * 1024
|
||||
content_length = download_resp.headers.get("content-length")
|
||||
if content_length:
|
||||
try:
|
||||
if int(content_length) > max_size:
|
||||
raise ValueError(
|
||||
f"文件 Content-Length {content_length} bytes 超过平台限制 {max_size} bytes (500KB)"
|
||||
)
|
||||
except ValueError as e:
|
||||
if "超过平台限制" in str(e):
|
||||
raise
|
||||
# 非数字时忽略
|
||||
|
||||
file_content = download_resp.content
|
||||
|
||||
# 2. 文件大小校验(平台限制 500KB)
|
||||
if len(file_content) > max_size:
|
||||
raise ValueError(
|
||||
f"文件大小 {len(file_content)} bytes 超过平台限制 {max_size} bytes (500KB),"
|
||||
"请先压缩或拆分后重试"
|
||||
)
|
||||
|
||||
# 3. 解析文件名并校验扩展名
|
||||
file_name = self._extract_file_name(download_resp, file_url)
|
||||
allowed_exts = (".xlsx", ".xls")
|
||||
if not file_name.lower().endswith(allowed_exts):
|
||||
raise ValueError(
|
||||
f"不支持的文件类型: {file_name},仅支持 {', '.join(allowed_exts)}"
|
||||
)
|
||||
|
||||
# 4. 推断 MIME 类型
|
||||
content_type, _ = mimetypes.guess_type(file_name)
|
||||
if not content_type:
|
||||
content_type = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
if file_name.lower().endswith(".xlsx")
|
||||
else "application/vnd.ms-excel"
|
||||
)
|
||||
|
||||
# 5. 构建文件上传
|
||||
files = {
|
||||
"file": (file_name, io.BytesIO(file_content), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
||||
"file": (file_name, io.BytesIO(file_content), content_type),
|
||||
}
|
||||
|
||||
return await self.client.upload(
|
||||
@@ -44,29 +90,235 @@ class PreviewImportDataTool(ToolDef):
|
||||
params={"target": target},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_file_name(response, file_url: str) -> str:
|
||||
"""从响应头或 URL 中提取文件名。"""
|
||||
# 优先从 Content-Disposition 解析
|
||||
content_disposition = response.headers.get("content-disposition")
|
||||
if content_disposition:
|
||||
# 简单解析 filename="xxx" 或 filename*=UTF-8''xxx
|
||||
# 注意:filename* 也包含 filename= 前缀,必须先检查 filename*
|
||||
for part in content_disposition.split(";"):
|
||||
part = part.strip()
|
||||
if part.lower().startswith("filename*="):
|
||||
encoded = part.split("=", 1)[1]
|
||||
# 形如 UTF-8''xxx
|
||||
if "''" in encoded:
|
||||
_, _, file_name = encoded.partition("''")
|
||||
return os.path.basename(unquote(file_name, encoding="utf-8"))
|
||||
return os.path.basename(unquote(encoded, encoding="utf-8"))
|
||||
if part.lower().startswith("filename="):
|
||||
file_name = part.split("=", 1)[1].strip('"')
|
||||
return os.path.basename(unquote(file_name))
|
||||
|
||||
# fallback 从 URL 路径获取
|
||||
parsed = urlparse(file_url)
|
||||
path = unquote(parsed.path)
|
||||
file_name = os.path.basename(path) if path else ""
|
||||
if file_name and "." in file_name:
|
||||
return file_name
|
||||
|
||||
return "import.xlsx"
|
||||
|
||||
|
||||
@register_tool("confirm_import_data")
|
||||
class ConfirmImportDataTool(ToolDef):
|
||||
name = "confirm_import_data"
|
||||
description = "确认导入 AI 识别后的数据"
|
||||
description = (
|
||||
"确认导入 AI 识别后的数据(建表+插数据),第二步。第一步先调 preview_import_data。\n"
|
||||
"【data 传什么】把 preview_import_data 返回的 data 原文整块传给 data 参数即可,"
|
||||
"工具会自动解包并组装成后端要求的 {tableStructure(单表对象,含databaseName), allData} 结构。\n"
|
||||
"data 的标准形态(= preview 的返回):\n"
|
||||
" {\n"
|
||||
" \"tableStructure\": { \"success\":true, \"message\":\"...\",\n"
|
||||
" \"data\": { \"tables\": [ { \"tableName\":\"animals\", \"columns\":[...] } ] },\n"
|
||||
" \"allData\": [ [列名表头行...], [行1各列值...], [行2各列值...] ] },\n"
|
||||
" \"databaseName\": \"目标库名\", \"target\": \"prod|test\"\n"
|
||||
" }\n"
|
||||
"【allData 的结构(关键)】allData 是二维数组:\n"
|
||||
" · 首行 allData[0] 是【表头行】= 各列的 columnName(列名),真实数据从 allData[1] 起;\n"
|
||||
" · 每行(含表头行)都是「按 columns 顺序排列的位置数组」,行宽 = 列总数(含 SERIAL 主键等所有列,不裁剪);\n"
|
||||
" · 若调用方传的 allData 没带表头行(首行就是数据),工具会据列名自动补一行表头——"
|
||||
"否则后端会把首行数据当成字段名,报「查询字段不存在/字段名称不正确」。\n"
|
||||
"【列名必须对应目标表真实字段】tableStructure.columns(及 allData 首行表头)里的列名,"
|
||||
"必须是目标表中确实存在的字段名(后端按列名拼 INSERT)。导入到已有表时,"
|
||||
"不要直接用 Excel 识别出的列,应先调 get_table_detail 拿到目标表真实字段,"
|
||||
"再把 columns、表头、各行取值对齐到这些真实字段,否则报「查询字段不存在/字段名称不正确」。\n"
|
||||
"databaseName/target 既可放顶层参数,也可放在 data 里,工具都能识别。"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源 ID"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "description": "环境"},
|
||||
"data": {"type": "object", "description": "导入数据(含 tableStructure + allData)"},
|
||||
"connectionId": {"type": "string", "description": "数据源连接 ID(同 preview 用的那个)"},
|
||||
"databaseName": {"type": "string", "description": "落库的数据库名(导入目标库)。必填——顶层不给会尝试从 data.databaseName 回捞"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test。可放顶层或 data 里"},
|
||||
"data": {
|
||||
"type": "object",
|
||||
"description": (
|
||||
"preview_import_data 返回的 data 原文整块(含 tableStructure{success,message,data:{tables:[...]}} 与 allData)。"
|
||||
"allData 为二维数组:首行是列名表头(allData[0])、数据行从 allData[1] 起,"
|
||||
"每行按 columns 顺序给出全部列的值,行宽 = 列总数(不裁剪自增列)。"
|
||||
"缺表头时工具会据列名自动补。也接受调用方已组装好的最终结构。"
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["connectionId", "data"],
|
||||
"required": ["connectionId", "databaseName", "data"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _build_body(data: dict, database_name) -> dict:
|
||||
"""把 preview 返回的 data 组装成 confirm 要求的 body。
|
||||
|
||||
后端真实结构(已真机探测确认):
|
||||
{ "tableStructure": <单表对象,含 columns 且需带 databaseName>, "allData": [...] }
|
||||
preview 返回的 data.tableStructure 是 {success,message,data:{tables:[...]}} 的包装,
|
||||
需取出 data.tables[0] 作为单表对象。
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
# 若传入的是 preview 工具的完整返回信封 {code, msg, data:{...}},先剥一层
|
||||
if "tableStructure" not in data and isinstance(data.get("data"), dict):
|
||||
inner_env = data["data"]
|
||||
if "tableStructure" in inner_env or "allData" in inner_env:
|
||||
data = inner_env
|
||||
|
||||
ts = data.get("tableStructure")
|
||||
single_table = None
|
||||
ts_inner = {}
|
||||
if isinstance(ts, dict):
|
||||
if "columns" in ts:
|
||||
# 已是单表对象(调用方自行组装过)
|
||||
single_table = dict(ts)
|
||||
else:
|
||||
# preview 包装:tableStructure.data.tables[0]
|
||||
ts_inner = ts.get("data") if isinstance(ts.get("data"), dict) else {}
|
||||
tables = ts_inner.get("tables") if isinstance(ts_inner, dict) else None
|
||||
if isinstance(tables, list) and tables:
|
||||
single_table = dict(tables[0])
|
||||
|
||||
if single_table is None:
|
||||
# 无法识别结构,原样透传(兼容调用方已构造好完整 body 的情况)
|
||||
return data
|
||||
|
||||
if database_name and not single_table.get("databaseName"):
|
||||
single_table["databaseName"] = database_name
|
||||
|
||||
# allData 可能落在多个层级(取决于调用方/preview 的嵌套方式),按优先级查找:
|
||||
# 1. data.allData —— 与 tableStructure 平级(约定的标准位置)
|
||||
# 2. tableStructure.allData —— 嵌在 tableStructure 包装内(真机/AI 常见误放)
|
||||
# 3. tableStructure.data.allData —— 嵌在内层 data 里
|
||||
# 注意:只接受 list;任何非 list(如内层 data 的 tables 包装对象)都视为未命中,
|
||||
# 避免把 dict 误当成行数据传给后端。
|
||||
all_data = None
|
||||
for candidate in (
|
||||
data.get("allData"),
|
||||
ts.get("allData") if isinstance(ts, dict) else None,
|
||||
ts_inner.get("allData") if isinstance(ts_inner, dict) else None,
|
||||
):
|
||||
if isinstance(candidate, list):
|
||||
all_data = candidate
|
||||
break
|
||||
if all_data is None:
|
||||
all_data = []
|
||||
|
||||
# 表头行:后端约定 allData[0] 是「表头行」(列名数组),真实数据从 allData[1] 起
|
||||
# (见前端 TableRecognition.vue handleComplete 与 CustomizeDbTable.vue validateDataColumns)。
|
||||
# 若调用方传的 allData 没带表头(首行就是数据),后端会把首行数据当成字段名,
|
||||
# 报「查询字段不存在/字段名称不正确」。这里据列名补出表头行。
|
||||
all_data = ConfirmImportDataTool._ensure_header(single_table.get("columns"), all_data)
|
||||
|
||||
return {"tableStructure": single_table, "allData": all_data}
|
||||
|
||||
@staticmethod
|
||||
def _column_names(columns):
|
||||
"""从列定义中按顺序提取列名数组。"""
|
||||
if not isinstance(columns, list):
|
||||
return []
|
||||
return [c.get("columnName") for c in columns if isinstance(c, dict) and c.get("columnName")]
|
||||
|
||||
@staticmethod
|
||||
def _ensure_header(columns, all_data):
|
||||
"""确保 allData[0] 是「表头行」(列名数组)。
|
||||
|
||||
后端约定:allData[0] 为表头(列名),真实数据行从 allData[1] 起;数据行按 columns
|
||||
顺序给出【全部列】的值(不裁剪自增列)。前端 TableRecognition.vue 在提交前总会把
|
||||
列名作为首行 push 进 allData。若调用方(含 AI)传来的 allData 首行已经是数据(缺表头),
|
||||
后端会把首行当列名解析,报「查询字段不存在/字段名称不正确」。这里据列名补表头:
|
||||
- 首行恰好等于列名数组 → 视为已带表头,原样返回
|
||||
- 否则 → 在最前面补一行列名
|
||||
"""
|
||||
names = ConfirmImportDataTool._column_names(columns)
|
||||
if not names or not isinstance(all_data, list) or not all_data:
|
||||
return all_data
|
||||
first = all_data[0]
|
||||
if isinstance(first, list) and list(first) == names:
|
||||
return all_data # 已带表头
|
||||
return [names, *all_data]
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
target = args.pop("target", "test")
|
||||
target = args.pop("target", None)
|
||||
database_name = args.pop("databaseName", None)
|
||||
data = args.pop("data")
|
||||
|
||||
# 容错:databaseName / target 可能被放进 data 里(AI 常把 preview 返回的整块连同
|
||||
# databaseName/target 一起塞进 data)。顶层没给时,从 data 里回捞,并清出 data
|
||||
# 避免污染最终 body。
|
||||
if isinstance(data, dict):
|
||||
if database_name is None and data.get("databaseName"):
|
||||
database_name = data.get("databaseName")
|
||||
if target is None and data.get("target"):
|
||||
target = data.get("target")
|
||||
data = {k: v for k, v in data.items() if k not in ("databaseName", "target")}
|
||||
|
||||
if target is None:
|
||||
target = "test"
|
||||
|
||||
body = self._build_body(data, database_name)
|
||||
|
||||
# 预检:把后端那两个含糊的报错(「导入数据不能为空」/「数据库名称不能为空」)
|
||||
# 提前在工具层拦下,给出可操作的提示(指明 allData/databaseName 该放哪),
|
||||
# 避免调用方对着后端原文反复试错。仅在 body 已被识别为标准结构时校验。
|
||||
if isinstance(body, dict) and "tableStructure" in body:
|
||||
if not body.get("allData"):
|
||||
raise ValueError(
|
||||
"导入数据为空:未能从 data 中解析到 allData(数据行)。"
|
||||
"请确认 allData 是一个非空数组,可放在 data.allData、"
|
||||
"data.tableStructure.allData 或 data.tableStructure.data.allData 任一层级。"
|
||||
)
|
||||
ts = body["tableStructure"]
|
||||
if isinstance(ts, dict) and not ts.get("databaseName"):
|
||||
raise ValueError(
|
||||
"缺少 databaseName(落库目标库名):请通过顶层参数 databaseName 传入,"
|
||||
"或放在 data.databaseName 中(工具会自动塞进表对象)。"
|
||||
)
|
||||
|
||||
# 行宽与表头校验:_ensure_header 已保证 allData[0] 是表头行(列名)。
|
||||
# 后端要求每行(含表头)宽度 = 列数(全部列,含自增列占位),且表头之外至少有 1 行数据。
|
||||
# 行宽对不上后端只会回含糊的「字段名称不正确/查询字段不存在」,这里提前报清楚。
|
||||
cols = ts.get("columns") if isinstance(ts, dict) else None
|
||||
all_data = body["allData"]
|
||||
if isinstance(cols, list) and cols:
|
||||
total = len(cols)
|
||||
# 表头之外至少要有一行真实数据
|
||||
data_rows = [r for r in all_data[1:] if isinstance(r, list)]
|
||||
if not data_rows:
|
||||
raise ValueError(
|
||||
"导入数据为空:allData 除表头行外没有任何数据行。"
|
||||
"allData 约定首行为表头(列名),真实数据从第 2 行起。"
|
||||
)
|
||||
for idx, row in enumerate(all_data):
|
||||
if isinstance(row, list) and len(row) != total:
|
||||
raise ValueError(
|
||||
f"第 {idx + 1} 行列数为 {len(row)},与表结构的 {total} 列不匹配。"
|
||||
"allData 每行(含表头行)都应按 columns 顺序给出全部列的值;"
|
||||
"首行须为列名表头,数据行从第 2 行起。请核对是否多/少了列。"
|
||||
)
|
||||
|
||||
return await self.client.post(
|
||||
f"/datasource/connection/{connection_id}/import_document/confirm",
|
||||
json_data=data,
|
||||
json_data=body,
|
||||
params={"target": target},
|
||||
)
|
||||
|
||||
@@ -45,6 +45,28 @@ class ListTablesTool(ToolDef):
|
||||
return await self.client.get("/datasource/table/list", params=params)
|
||||
|
||||
|
||||
@register_tool("list_tables_with_ai")
|
||||
class ListTablesWithAiTool(ToolDef):
|
||||
name = "list_tables_with_ai"
|
||||
description = (
|
||||
"获取表元数据列表(含 AI 训练描述),内置库管理的主列表。"
|
||||
"注意:datasourceId 实为「配置/库」ID(即 create_connection_config 落库后的 config id)"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceId": {"type": "string", "description": "配置/库 ID(非数据源连接 ID)"},
|
||||
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||
},
|
||||
"required": ["datasourceId"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
params = {k: v for k, v in args.items() if v is not None}
|
||||
return await self.client.get("/datasource/table/ailist", params=params)
|
||||
|
||||
|
||||
@register_tool("get_table_detail")
|
||||
class GetTableDetailTool(ToolDef):
|
||||
name = "get_table_detail"
|
||||
@@ -117,8 +139,8 @@ class AlterTableTool(ToolDef):
|
||||
"items": {
|
||||
"type": "object",
|
||||
"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": "变更类型"},
|
||||
"column": {"type": "object", "description": "列定义(根据 operation 不同包含不同字段)"},
|
||||
"operation": {"type": "string", "enum": ["ADD_COLUMN", "MODIFY_COLUMN", "DROP_COLUMN"], "description": "变更类型:新增/修改/删除字段(后端实测仅支持这三种)"},
|
||||
"column": {"type": "object", "description": "列定义。ADD/MODIFY 需 columnName+columnType(+columnLength/columnComment 等);DROP 仅需 columnName"},
|
||||
},
|
||||
"required": ["operation", "column"],
|
||||
},
|
||||
@@ -137,7 +159,7 @@ class AlterTableTool(ToolDef):
|
||||
@register_tool("generate_table_by_description")
|
||||
class GenerateTableByDescriptionTool(ToolDef):
|
||||
name = "generate_table_by_description"
|
||||
description = "通过自然语言描述让 AI 生成表结构(异步任务)"
|
||||
description = "通过自然语言描述让 AI 生成表结构(异步任务,返回 taskId)。需配合 get_ai_training_detail 轮询 taskId 获取生成结果"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -149,3 +171,116 @@ class GenerateTableByDescriptionTool(ToolDef):
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.post("/datasource/connection/generate_table", json_data=args)
|
||||
|
||||
|
||||
@register_tool("create_database")
|
||||
class CreateDatabaseTool(ToolDef):
|
||||
name = "create_database"
|
||||
description = "在内置数据源下创建一个数据库(建表前置步骤)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||
"databaseName": {"type": "string", "description": "数据库名(仅字母数字下划线)"},
|
||||
},
|
||||
"required": ["connectionId", "databaseName"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
return await self.client.post(f"/datasource/connection/{connection_id}/create_database", json_data=args)
|
||||
|
||||
|
||||
@register_tool("create_database_table")
|
||||
class CreateDatabaseTableTool(ToolDef):
|
||||
name = "create_database_table"
|
||||
description = (
|
||||
"一次性创建数据库 + 表(合并接口,免去先建库再建表)。"
|
||||
"支持两种用法:①单表便捷参数 tableName/tableComment/columns;"
|
||||
"②批量传 tables 数组(一次建多张表)。二者任选其一"
|
||||
)
|
||||
# 单字段(columnName/columnType/...)的 schema,单表和批量两种用法共用
|
||||
_COLUMN_ITEMS = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"columnName": {"type": "string", "description": "字段名"},
|
||||
"columnType": {"type": "string", "description": "字段类型(VARCHAR/INTEGER/SERIAL等)"},
|
||||
"columnLength": {"type": "integer", "description": "字段长度"},
|
||||
"isPrimaryKey": {"type": "boolean", "description": "是否主键"},
|
||||
"isNullable": {"type": "boolean", "description": "是否可空"},
|
||||
"isAutoIncrement": {"type": "boolean", "description": "是否自增"},
|
||||
"columnComment": {"type": "string", "description": "字段注释"},
|
||||
"defaultValue": {"type": "string", "description": "默认值"},
|
||||
},
|
||||
"required": ["columnName", "columnType"],
|
||||
}
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||
"databaseName": {"type": "string", "description": "数据库名(仅字母数字下划线)"},
|
||||
# —— 用法①:单表便捷参数 ——
|
||||
"tableName": {"type": "string", "description": "表名(用法①单表,小写字母+数字+下划线)"},
|
||||
"tableComment": {"type": "string", "description": "表注释(用法①单表)"},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"description": "字段定义数组(用法①单表,结构同 create_table)",
|
||||
"items": _COLUMN_ITEMS,
|
||||
},
|
||||
# —— 用法②:批量 tables 数组 ——
|
||||
"tables": {
|
||||
"type": "array",
|
||||
"description": "表定义数组(用法②批量建多表),每项含 tableName/tableComment/columns",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableName": {"type": "string", "description": "表名"},
|
||||
"tableComment": {"type": "string", "description": "表注释"},
|
||||
"columns": {"type": "array", "description": "字段定义数组", "items": _COLUMN_ITEMS},
|
||||
},
|
||||
"required": ["tableName", "columns"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["connectionId", "databaseName"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
# 后端真实 body 结构:{databaseName, tables:[{tableName,tableComment,columns}]}(已真机探测确认)
|
||||
if not args.get("tables"):
|
||||
table = {"tableName": args.pop("tableName", None), "columns": args.pop("columns", [])}
|
||||
if args.get("tableComment") is not None:
|
||||
table["tableComment"] = args.pop("tableComment")
|
||||
args["tables"] = [table]
|
||||
else:
|
||||
# 批量用法下,清掉可能并存的单表字段,避免歧义
|
||||
args.pop("tableName", None)
|
||||
args.pop("tableComment", None)
|
||||
args.pop("columns", None)
|
||||
return await self.client.post(f"/datasource/connection/{connection_id}/create_database_table", json_data=args)
|
||||
|
||||
|
||||
@register_tool("alter_database")
|
||||
class AlterDatabaseTool(ToolDef):
|
||||
name = "alter_database"
|
||||
description = "修改数据库(如改名)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||
"databaseName": {"type": "string", "description": "当前数据库名"},
|
||||
"newName": {"type": "string", "description": "新数据库名"},
|
||||
},
|
||||
"required": ["connectionId", "databaseName", "newName"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
# 后端改名字段名为 newName(已真机探测确认);兼容历史传参 newDatabaseName
|
||||
if "newName" not in args and "newDatabaseName" in args:
|
||||
args["newName"] = args.pop("newDatabaseName")
|
||||
return await self.client.put(f"/datasource/connection/{connection_id}/alter_database", json_data=args)
|
||||
|
||||
@@ -71,7 +71,7 @@ class CreateDatasourceTool(ToolDef):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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": "数据库地址"},
|
||||
"port": {"type": "integer", "description": "端口号"},
|
||||
"databaseName": {"type": "string", "description": "要连接的数据库名"},
|
||||
@@ -88,7 +88,7 @@ class CreateDatasourceTool(ToolDef):
|
||||
args = dict(args)
|
||||
test_first = args.pop("test_first", True)
|
||||
|
||||
# 如果需要先测试连接
|
||||
# 如果需要先测试连接(测试失败时 client 会直接抛异常,由上层统一捕获)
|
||||
if test_first:
|
||||
test_data = {
|
||||
"datasourceName": args.get("datasourceName"),
|
||||
@@ -100,9 +100,7 @@ class CreateDatasourceTool(ToolDef):
|
||||
"password": args.get("password"),
|
||||
"connectionType": args.get("connectionType", "user_password"),
|
||||
}
|
||||
test_result = 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', '未知错误')}"}
|
||||
await self.client.post("/datasource/connection/test", json_data=test_data)
|
||||
|
||||
# 创建数据源
|
||||
return await self.client.post("/datasource/connection", json_data=args)
|
||||
@@ -117,11 +115,13 @@ class UpdateDatasourceTool(ToolDef):
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "数据源 ID"},
|
||||
"datasourceName": {"type": "string", "description": "更新名称"},
|
||||
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
|
||||
"host": {"type": "string", "description": "更新地址"},
|
||||
"port": {"type": "integer", "description": "更新端口"},
|
||||
"databaseName": {"type": "string", "description": "更新数据库名"},
|
||||
"username": {"type": "string", "description": "更新用户名"},
|
||||
"password": {"type": "string", "description": "新密码(不传则不变)"},
|
||||
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型"},
|
||||
"remark": {"type": "string", "description": "更新描述"},
|
||||
},
|
||||
"required": ["id"],
|
||||
@@ -171,3 +171,79 @@ class DeleteDatasourceTool(ToolDef):
|
||||
|
||||
# 删除数据源
|
||||
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")
|
||||
@@ -1,5 +1,9 @@
|
||||
"""
|
||||
技能与工具管理工具 (工具 24-29)
|
||||
技能与工具管理工具
|
||||
|
||||
把 SQL 沉淀为数据源可复用工具的入口统一走 add_sql_tool_to_datasource(保证技能必有工具,
|
||||
内部按需建技能+写配置+建工具)。其余为底层积木:查询类、向已有技能加/改工具、改技能配置。
|
||||
不单独暴露「只建技能」工具,避免产生无效空技能。
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -39,24 +43,6 @@ class GetSkillToolsTool(ToolDef):
|
||||
return await self.client.get(f"/datasource/skill/getBySkillId/{args['skillId']}")
|
||||
|
||||
|
||||
@register_tool("create_skill")
|
||||
class CreateSkillTool(ToolDef):
|
||||
name = "create_skill"
|
||||
description = "为数据源创建技能"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||
"name": {"type": "string", "description": "技能名称(不传则自动生成)"},
|
||||
"description": {"type": "string", "description": "技能描述"},
|
||||
},
|
||||
"required": ["datasourceId"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.post("/datasource/skill/createOrGet", json_data=args)
|
||||
|
||||
|
||||
@register_tool("create_sql_tool")
|
||||
class CreateSqlToolTool(ToolDef):
|
||||
name = "create_sql_tool"
|
||||
@@ -79,7 +65,7 @@ class CreateSqlToolTool(ToolDef):
|
||||
"name": {"type": "string", "description": "工具名称"},
|
||||
"businessDescription": {"type": "string", "description": "业务描述"},
|
||||
"sqlTemplate": {"type": "string", "description": "SQL 模板(支持 #{param} 参数占位)"},
|
||||
"sqlParams": {"type": "string", "description": "参数 JSON Schema(JSON 字符串或对象)"},
|
||||
"sqlParams": {"type": "string", "description": "参数定义,最终以 JSON 字符串提交(传 dict 会自动 json.dumps)。内容形态后端不挑剔:可为 JSON Schema 对象串如 {\"type\":\"object\",...},也可为字段定义数组串;无参数传 \"{}\""},
|
||||
"resultType": {"type": "string", "enum": ["single", "list"], "default": "list", "description": "结果类型,默认 list"},
|
||||
"businessScenario": {"type": "string", "description": "业务场景描述"},
|
||||
},
|
||||
@@ -92,11 +78,17 @@ class CreateSqlToolTool(ToolDef):
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
# 处理 suggestions 中的 sqlParams
|
||||
# tableIds 后端始终期望该键存在:前端真机始终传 ""(空串)。
|
||||
# 调用方未给时补 "",与前端 postSqlSkillConfirmTools 的请求保持一致。
|
||||
if "tableIds" not in args or args["tableIds"] is None:
|
||||
args["tableIds"] = ""
|
||||
# 处理 suggestions 中的 sqlParams:dict 自动序列化为 JSON 字符串;
|
||||
# 同时补齐 resultType 默认值 list(与前端默认一致)。
|
||||
if "suggestions" in args and isinstance(args["suggestions"], list):
|
||||
for suggestion in args["suggestions"]:
|
||||
if "sqlParams" in suggestion and isinstance(suggestion["sqlParams"], dict):
|
||||
suggestion["sqlParams"] = json.dumps(suggestion["sqlParams"])
|
||||
suggestion.setdefault("resultType", "list")
|
||||
return await self.client.post("/datasource/skill/confirmTools", json_data=args)
|
||||
|
||||
|
||||
@@ -119,19 +111,276 @@ class DeleteSkillToolTool(ToolDef):
|
||||
@register_tool("update_skill_config")
|
||||
class UpdateSkillConfigTool(ToolDef):
|
||||
name = "update_skill_config"
|
||||
description = "更新技能配置(如 MCP Server 配置模板)"
|
||||
description = (
|
||||
"更新技能(名称/描述/MCP 配置模板等)。对应后端 skill/updateOrGet。"
|
||||
"datasourceId 与 skillId 均必填且为真实 ID(来自其他工具返回,不可臆造)。"
|
||||
"若不显式传 configTemplate,会按这两个 ID 自动生成 lzwcai-mcp-sqlexecutor 的标准 MCP 配置模板(与前端一致)。"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||
"configTemplate": {"type": "string", "description": "配置模板 JSON 字符串"},
|
||||
"datasourceId": {
|
||||
"type": "string",
|
||||
"description": "数据源/配置 ID(真实 ID,来自 list_databases / list_tables_with_ai / get_connection_config_list,不可臆造)",
|
||||
},
|
||||
"skillId": {
|
||||
"type": "string",
|
||||
"description": "技能 ID(真实 ID,来自 get_skill_by_datasource 的返回,不可臆造;与 datasourceId 一起用于生成 configTemplate)",
|
||||
},
|
||||
"name": {"type": "string", "description": "技能名称(可选)"},
|
||||
"description": {"type": "string", "description": "技能描述(可选)"},
|
||||
"configTemplate": {
|
||||
"type": "string",
|
||||
"description": "配置模板 JSON 字符串(可选)。不传时按 datasourceId + skillId 自动生成标准模板",
|
||||
},
|
||||
},
|
||||
"required": ["datasourceId", "configTemplate"],
|
||||
"required": ["datasourceId", "skillId"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _build_config_template(datasource_id: str, skill_id: str) -> str:
|
||||
"""生成 lzwcai-mcp-sqlexecutor 的标准 MCP 配置模板。
|
||||
|
||||
模板大部分是固定值,仅 mcpServerKey 后缀、env.databaseId、env.skillId 随
|
||||
datasourceId / skillId 动态变化(与前端 SqlControllerMsg.vue 的 configTemplateObj 完全一致)。
|
||||
"""
|
||||
mcp_server_key = f"lzwcai_mcp_sqlexecutor_{datasource_id}"
|
||||
config_obj = {
|
||||
"mcpServers": {
|
||||
mcp_server_key: {
|
||||
"command": "uvx",
|
||||
"type": "stdio",
|
||||
"args": ["lzwcai-mcp-sqlexecutor"],
|
||||
"tiemout": 200,
|
||||
"env": {
|
||||
"databaseId": datasource_id,
|
||||
"skillId": skill_id,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
return json.dumps(config_obj)
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
# 如果 configTemplate 是 dict,转为 JSON 字符串
|
||||
if "configTemplate" in args and isinstance(args["configTemplate"], dict):
|
||||
args["configTemplate"] = json.dumps(args["configTemplate"])
|
||||
# 未显式提供 configTemplate 时,按 datasourceId + skillId 自动生成标准模板
|
||||
elif not args.get("configTemplate"):
|
||||
args["configTemplate"] = self._build_config_template(
|
||||
str(args["datasourceId"]), str(args["skillId"])
|
||||
)
|
||||
return await self.client.post("/datasource/skill/updateOrGet", json_data=args)
|
||||
|
||||
|
||||
@register_tool("update_skill_tool")
|
||||
class UpdateSkillToolTool(ToolDef):
|
||||
name = "update_skill_tool"
|
||||
description = (
|
||||
"修改技能下某个工具的名称/描述/SQL等(对应后端 tskilltool/updateOrGet,upsert 语义)。"
|
||||
"改展示名传 name 即可(工具会同时写 name 和 uniqueName 两个字段,与工具实体存储一致)。"
|
||||
"工具名建议遵循前端约束:≤20 字、只含中英文/数字/空格、不含特殊符号。"
|
||||
)
|
||||
# 工具实体同时存在 name 与 uniqueName 两个字段(真机数据里二者取值相同,见 skill.json)。
|
||||
# 前端 ChatDebugging.vue handleEditToolSubmit 改名时提交 {id, name, description}(用 name);
|
||||
# 早期真机探测又得到 uniqueName。为消除歧义、与实体存储保持一致,改名时两个字段都写。
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "技能工具 ID(get_skill_tools 返回的 id)"},
|
||||
"name": {"type": "string", "description": "工具展示名(可选)。会同时写入 name 与 uniqueName"},
|
||||
"description": {"type": "string", "description": "工具描述(可选)"},
|
||||
"sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"},
|
||||
"resultType": {"type": "string", "enum": ["single", "list"], "description": "结果类型(可选)"},
|
||||
},
|
||||
"required": ["id"],
|
||||
}
|
||||
|
||||
# 旧参数名 -> 真实字段名(向后兼容此前错误实现的调用方)
|
||||
_LEGACY_MAP = {
|
||||
"skillToolId": "id",
|
||||
"businessDescription": "description",
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
for old, new in self._LEGACY_MAP.items():
|
||||
if old in args and new not in args:
|
||||
args[new] = args.pop(old)
|
||||
else:
|
||||
args.pop(old, None)
|
||||
# 展示名:name / uniqueName 任一传入都同步到两个字段(与工具实体存储一致,
|
||||
# 兼容前端用 name、早期探测用 uniqueName 两种契约,避免改名不生效)。
|
||||
display_name = args.get("name") if args.get("name") is not None else args.get("uniqueName")
|
||||
if display_name is not None:
|
||||
args["name"] = display_name
|
||||
args["uniqueName"] = display_name
|
||||
# businessScenario 后端实体无此字段,丢弃避免干扰
|
||||
args.pop("businessScenario", None)
|
||||
return await self.client.post("/datasource/skill/tskilltool/updateOrGet", json_data=args)
|
||||
|
||||
|
||||
@register_tool("add_sql_tool_to_datasource")
|
||||
class AddSqlToolToDatasourceTool(ToolDef):
|
||||
name = "add_sql_tool_to_datasource"
|
||||
description = (
|
||||
"把一条 SQL 沉淀为数据源的可复用工具(一步到位,推荐用这个而不是手动拼 "
|
||||
"update_skill_config/create_sql_tool)。\n"
|
||||
"【为什么用它】技能(skill)必须挂着工具才有效,单独建技能会留下无效的空技能。本工具内部"
|
||||
"1:1 复刻前端 handleAddToolSubmit 的完整链路,保证技能必有工具:\n"
|
||||
" 读 skillBool → 技能不存在则 createOrGet 建技能 → getByDatasource 拿真实 skillId →"
|
||||
" 按 sqlTemplate 去重 → 技能新建时写 lzwcai-mcp-sqlexecutor 配置模板 → confirmTools 建工具。\n"
|
||||
"【幂等/去重】同一 datasourceId 下若已存在 sqlTemplate 相同的工具,直接返回 skipped,不重复创建。\n"
|
||||
"datasourceId 必填且为真实 ID(来自 list_databases / list_tables_with_ai / get_connection_config_list)。"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceId": {"type": "string", "description": "数据源/配置 ID(真实 ID,不可臆造)"},
|
||||
"name": {"type": "string", "description": "工具名称(展示名)"},
|
||||
"businessDescription": {"type": "string", "description": "工具的业务描述"},
|
||||
"sqlTemplate": {"type": "string", "description": "SQL 模板(支持 #{param} 参数占位)"},
|
||||
"sqlParams": {
|
||||
"type": "string",
|
||||
"description": "参数定义,最终以 JSON 字符串提交(传 dict 会自动 json.dumps)。内容形态后端不挑剔(JSON Schema 对象串或字段定义数组串均可);不传默认空 schema",
|
||||
},
|
||||
"resultType": {
|
||||
"type": "string",
|
||||
"enum": ["single", "list"],
|
||||
"default": "list",
|
||||
"description": "结果类型,默认 list",
|
||||
},
|
||||
"businessScenario": {"type": "string", "description": "业务场景描述(可选)"},
|
||||
"tableIds": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": "关联的表 ID 数组(可选)",
|
||||
},
|
||||
"skillName": {"type": "string", "description": "技能不存在时新建技能用的名称(可选,不传自动生成)"},
|
||||
"skillDescription": {"type": "string", "description": "技能不存在时新建技能用的描述(可选)"},
|
||||
},
|
||||
"required": ["datasourceId", "name", "businessDescription", "sqlTemplate"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _unwrap(resp):
|
||||
"""从 {code,msg,data} 信封里取 data;非信封则原样返回。"""
|
||||
if isinstance(resp, dict) and "data" in resp and ("code" in resp or "msg" in resp):
|
||||
return resp["data"]
|
||||
return resp
|
||||
|
||||
@staticmethod
|
||||
def _normalize_sql(sql) -> str:
|
||||
"""归一化 SQL 用于去重比较:折叠空白 + strip(与前端 replace(/\\s+/g,' ').trim() 一致)。"""
|
||||
if not isinstance(sql, str):
|
||||
return ""
|
||||
return " ".join(sql.split()).strip()
|
||||
|
||||
async def _get_skill_id(self, datasource_id: str):
|
||||
"""getByDatasource 拿技能 id;拿不到返回 None。"""
|
||||
resp = await self.client.get(f"/datasource/skill/getByDatasource/{datasource_id}")
|
||||
data = self._unwrap(resp)
|
||||
if isinstance(data, dict):
|
||||
return data.get("id")
|
||||
return None
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
datasource_id = str(args["datasourceId"])
|
||||
sql_template = args["sqlTemplate"]
|
||||
|
||||
# 1. 读数据源配置,判断 skillBool(技能是否已存在)
|
||||
config_resp = await self.client.get(f"/datasource/config/{datasource_id}")
|
||||
config_data = self._unwrap(config_resp)
|
||||
skill_bool = config_data.get("skillBool") if isinstance(config_data, dict) else None
|
||||
|
||||
created_skill = False
|
||||
# 2. 技能不存在 → 先建技能(createOrGet)
|
||||
if skill_bool is not True:
|
||||
create_skill_body = {"datasourceId": datasource_id}
|
||||
if args.get("skillName"):
|
||||
create_skill_body["name"] = args["skillName"]
|
||||
if args.get("skillDescription"):
|
||||
create_skill_body["description"] = args["skillDescription"]
|
||||
await self.client.post("/datasource/skill/createOrGet", json_data=create_skill_body)
|
||||
created_skill = True
|
||||
|
||||
# 3. 拿真实 skillId(注意:id 来自 getByDatasource,不是 createOrGet 的返回)
|
||||
skill_id = await self._get_skill_id(datasource_id)
|
||||
if not skill_id:
|
||||
raise ValueError(
|
||||
f"未能获取数据源 {datasource_id} 的技能 ID(getByDatasource 未返回 id)。"
|
||||
"请确认 datasourceId 正确、技能创建是否成功。"
|
||||
)
|
||||
skill_id = str(skill_id)
|
||||
|
||||
# 4. 去重:同 sqlTemplate 的工具已存在则跳过
|
||||
tools_resp = await self.client.get(f"/datasource/skill/getBySkillId/{skill_id}")
|
||||
tools_data = self._unwrap(tools_resp)
|
||||
target_norm = self._normalize_sql(sql_template)
|
||||
if isinstance(tools_data, list):
|
||||
for tool in tools_data:
|
||||
if isinstance(tool, dict) and self._normalize_sql(tool.get("sqlTemplate")) == target_norm:
|
||||
return {
|
||||
"skipped": True,
|
||||
"reason": "已存在 sqlTemplate 相同的工具,未重复创建",
|
||||
"skillId": skill_id,
|
||||
"existingTool": {
|
||||
"id": tool.get("id"),
|
||||
"uniqueName": tool.get("uniqueName") or tool.get("name"),
|
||||
},
|
||||
}
|
||||
|
||||
# 5. 技能是本次新建的 → 写入 lzwcai-mcp-sqlexecutor 标准配置模板
|
||||
if created_skill:
|
||||
config_template = UpdateSkillConfigTool._build_config_template(datasource_id, skill_id)
|
||||
await self.client.post(
|
||||
"/datasource/skill/updateOrGet",
|
||||
json_data={"datasourceId": datasource_id, "configTemplate": config_template},
|
||||
)
|
||||
|
||||
# 6. confirmTools 建工具
|
||||
sql_params = args.get("sqlParams")
|
||||
if isinstance(sql_params, dict):
|
||||
sql_params = json.dumps(sql_params)
|
||||
elif not sql_params:
|
||||
sql_params = '{"type":"object","required":[],"properties":{}}'
|
||||
|
||||
suggestion = {
|
||||
"name": args["name"],
|
||||
"businessDescription": args["businessDescription"],
|
||||
"sqlTemplate": sql_template,
|
||||
"sqlParams": sql_params,
|
||||
"resultType": args.get("resultType", "list"),
|
||||
"businessScenario": args.get("businessScenario", "数据查询场景"),
|
||||
}
|
||||
# tableIds:前端真机始终传 ""(空串)。None / 空列表都归一为 "",与前端一致;
|
||||
# 仅当调用方显式给了非空列表时才透传该列表。
|
||||
table_ids = args.get("tableIds")
|
||||
confirm_body = {
|
||||
"skillId": skill_id,
|
||||
"tableIds": table_ids if table_ids else "",
|
||||
"suggestions": [suggestion],
|
||||
}
|
||||
try:
|
||||
confirm_result = await self.client.post(
|
||||
"/datasource/skill/confirmTools", json_data=confirm_body
|
||||
)
|
||||
except Exception as e:
|
||||
# 工具创建失败:若本次刚建了技能,此刻技能是「空技能」(有配置无工具)——
|
||||
# 正是要避免的无效状态。明确告知调用方:技能已建好,重跑本工具即可补上工具
|
||||
# (重跑会走 skillBool=true 分支,仅补工具,幂等安全)。
|
||||
if created_skill:
|
||||
raise Exception(
|
||||
f"技能已创建(skillId={skill_id})但工具创建失败:{e}。"
|
||||
"当前技能为「空技能」,请用相同参数重新调用本工具补上工具"
|
||||
"(重跑只会补工具、不会重复建技能)。"
|
||||
) from e
|
||||
raise
|
||||
return {
|
||||
"success": True,
|
||||
"skillId": skill_id,
|
||||
"skillCreated": created_skill,
|
||||
"result": confirm_result,
|
||||
}
|
||||
|
||||
@@ -35,4 +35,11 @@ class ExecuteSqlTool(ToolDef):
|
||||
body["businessName"] = args["businessName"]
|
||||
if "params" in args:
|
||||
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")
|
||||
class ToggleTableSubscriptionTool(ToolDef):
|
||||
name = "toggle_table_subscription"
|
||||
description = "切换表的订阅状态"
|
||||
description = "切换表的订阅状态(订阅依赖该表已配置 MQTT 字段关联,否则后端会报操作失败)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableId": {"type": "string", "description": "表 ID"},
|
||||
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||
"configId": {"type": "string", "description": "库/配置 ID(即 list_databases 返回的 config id)"},
|
||||
"tableName": {"type": "string", "description": "表名(注意:后端按表名而非 tableId 识别)"},
|
||||
"subscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"},
|
||||
},
|
||||
"required": ["tableId", "datasourceId", "subscribe"],
|
||||
"required": ["configId", "tableName", "subscribe"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
# 映射参数名为后端 API 期望的格式
|
||||
# 后端真实字段(已真机探测确认):configId + tableName + isSubscribe(bool)
|
||||
# 兼容旧参数名 datasourceId->configId
|
||||
config_id = args.get("configId") or args.get("datasourceId")
|
||||
body = {
|
||||
"tableId": args["tableId"],
|
||||
"datasourceId": args["datasourceId"],
|
||||
"subscribe": args["subscribe"],
|
||||
"configId": config_id,
|
||||
"tableName": args.get("tableName"),
|
||||
"isSubscribe": args.get("subscribe"),
|
||||
}
|
||||
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")
|
||||
class QueryTableDataTool(ToolDef):
|
||||
name = "query_table_data"
|
||||
description = "查询内置表数据(分页)"
|
||||
description = "查询内置表数据(分页)。target 默认 test(调试环境);查询线上数据须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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": "页码"},
|
||||
"pageSize": {"type": "integer", "default": 10, "description": "每页数量"},
|
||||
},
|
||||
@@ -25,6 +25,7 @@ class QueryTableDataTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
args.setdefault("target", "test")
|
||||
params = {k: v for k, v in args.items() if v is not None}
|
||||
return await self.client.get(f"/datasource/connection/builtin/table/{table_id}", params=params)
|
||||
|
||||
@@ -32,12 +33,12 @@ class QueryTableDataTool(ToolDef):
|
||||
@register_tool("insert_table_row")
|
||||
class InsertTableRowTool(ToolDef):
|
||||
name = "insert_table_row"
|
||||
description = "向内置表插入一行数据"
|
||||
description = "向内置表插入一行数据。⚠️ 写操作 target 默认 test(调试环境);写入线上库须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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": "行数据(键值对,键为字段名)"},
|
||||
},
|
||||
"required": ["tableId", "data"],
|
||||
@@ -46,21 +47,22 @@ class InsertTableRowTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
target = args.pop("target", "prod")
|
||||
target = args.pop("target", "test")
|
||||
data = args.pop("data", {})
|
||||
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")
|
||||
class UpdateTableRowTool(ToolDef):
|
||||
name = "update_table_row"
|
||||
description = "更新内置表的指定行"
|
||||
description = "更新内置表的指定行。⚠️ 写操作 target 默认 test(调试环境);修改线上库须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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})"},
|
||||
"data": {"type": "object", "description": "要更新的字段值"},
|
||||
},
|
||||
@@ -70,7 +72,7 @@ class UpdateTableRowTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
target = args.pop("target", "prod")
|
||||
target = args.pop("target", "test")
|
||||
primary_key = args.pop("primaryKey")
|
||||
data = args.pop("data", {})
|
||||
params = {"target": target} if target else {}
|
||||
@@ -81,12 +83,12 @@ class UpdateTableRowTool(ToolDef):
|
||||
@register_tool("delete_table_rows")
|
||||
class DeleteTableRowsTool(ToolDef):
|
||||
name = "delete_table_rows"
|
||||
description = "删除内置表的指定行(根据主键批量删除)"
|
||||
description = "删除内置表的指定行(根据主键批量删除)。⚠️ 写操作 target 默认 test(调试环境);删除线上库数据须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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": {
|
||||
"type": "array",
|
||||
"description": "主键数组(如 [{\"id\": 1}, {\"id\": 2}])",
|
||||
@@ -99,7 +101,7 @@ class DeleteTableRowsTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
target = args.pop("target", "prod")
|
||||
target = args.pop("target", "test")
|
||||
primary_keys = args.pop("primaryKeys")
|
||||
params = {"target": target} if target else {}
|
||||
body = {"primaryKeys": primary_keys}
|
||||
@@ -109,12 +111,12 @@ class DeleteTableRowsTool(ToolDef):
|
||||
@register_tool("export_table_excel")
|
||||
class ExportTableExcelTool(ToolDef):
|
||||
name = "export_table_excel"
|
||||
description = "导出表数据为 Excel 文件(返回 base64 编码)"
|
||||
description = "导出表数据为 Excel 文件(返回 base64 编码)。target 默认 test(调试环境);导出线上数据须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"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"],
|
||||
}
|
||||
@@ -122,7 +124,7 @@ class ExportTableExcelTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
target = args.pop("target", "prod")
|
||||
target = args.pop("target", "test")
|
||||
params = {"target": target} if target else {}
|
||||
result = await self.client.get(f"/datasource/connection/builtin/table/{table_id}/export/excel", params=params)
|
||||
|
||||
|
||||
1139
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt
Normal file
1139
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,10 @@
|
||||
from .env_config import get_api_key, get_base_url, get_env_config
|
||||
from .env_config import (
|
||||
get_api_key,
|
||||
get_base_url,
|
||||
get_env_config,
|
||||
get_account,
|
||||
get_password,
|
||||
)
|
||||
from .logger_config import setup_system_logging, get_logger
|
||||
from .api_client import AgileDBAPIClient, get_default_client
|
||||
|
||||
@@ -6,6 +12,8 @@ __all__ = [
|
||||
'get_api_key',
|
||||
'get_base_url',
|
||||
'get_env_config',
|
||||
'get_account',
|
||||
'get_password',
|
||||
'setup_system_logging',
|
||||
'get_logger',
|
||||
'AgileDBAPIClient',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -3,12 +3,17 @@
|
||||
用于调用数据库管理平台的所有 API 接口
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import httpx
|
||||
import json
|
||||
import os
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from .env_config import get_api_key, get_base_url
|
||||
from .env_config import (
|
||||
get_api_key,
|
||||
get_base_url,
|
||||
get_account,
|
||||
get_password,
|
||||
)
|
||||
from .logger_config import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
@@ -16,14 +21,29 @@ logger = get_logger(__name__)
|
||||
# 默认超时配置(秒)
|
||||
DEFAULT_TIMEOUT = 30.0
|
||||
|
||||
# 登录接口路径(base_url 已含 /api 前缀,此处不重复带)
|
||||
LOGIN_PATH = "/login"
|
||||
|
||||
# 登录类型(平台固定为 user)
|
||||
LOGIN_TYPE = "user"
|
||||
|
||||
|
||||
class AgileDBAPIClient:
|
||||
"""数据库管理平台 API 客户端"""
|
||||
"""数据库管理平台 API 客户端
|
||||
|
||||
认证支持两种方式(优先级从高到低):
|
||||
1. 显式 api_key / 环境变量 API_KEY —— 直接作为 Bearer token 使用;
|
||||
2. 账号密码(环境变量 AGILE_DB_ACCOUNT / AGILE_DB_PASSWORD)—— 懒登录,
|
||||
首次请求时自动调用 /login 换取 token 并缓存;token 失效(401)时
|
||||
自动重新登录并重试一次。
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
account: Optional[str] = None,
|
||||
password: Optional[str] = None,
|
||||
default_timeout: float = DEFAULT_TIMEOUT,
|
||||
):
|
||||
"""
|
||||
@@ -31,21 +51,36 @@ class AgileDBAPIClient:
|
||||
|
||||
Args:
|
||||
base_url: API 基础 URL(默认从环境变量 backendBaseUrl 读取)
|
||||
api_key: API 密钥(默认从环境变量 API_KEY 读取)
|
||||
api_key: API 密钥(默认从环境变量 API_KEY 读取,可为空)
|
||||
account: 登录账号(默认从环境变量 AGILE_DB_ACCOUNT 读取)
|
||||
password: 登录密码(默认从环境变量 AGILE_DB_PASSWORD 读取)
|
||||
default_timeout: 请求超时时间(秒),默认 30 秒
|
||||
"""
|
||||
if base_url is None:
|
||||
base_url = get_base_url()
|
||||
self.base_url = (base_url if base_url is not None else get_base_url()).rstrip('/')
|
||||
# 显式配置的 api_key 直接作为 token 使用(去掉可能存在的 Bearer 前缀,统一在 _get_headers 拼)
|
||||
explicit_key = api_key if api_key is not None else get_api_key()
|
||||
self._token: Optional[str] = self._strip_bearer(explicit_key) or None
|
||||
|
||||
if api_key is None:
|
||||
api_key = get_api_key()
|
||||
self.account = account if account is not None else get_account()
|
||||
self.password = password if password is not None else get_password()
|
||||
|
||||
self.base_url = base_url.rstrip('/')
|
||||
self.api_key = api_key
|
||||
self.default_timeout = default_timeout
|
||||
self._client: Optional[httpx.AsyncClient] = None
|
||||
# 串行化登录,避免并发请求同时触发多次登录
|
||||
self._login_lock = asyncio.Lock()
|
||||
|
||||
logger.info(f"[客户端初始化] base_url={self.base_url}")
|
||||
logger.info(
|
||||
f"[客户端初始化] base_url={self.base_url}, "
|
||||
f"认证方式={'api_key' if self._token else ('account:' + self.account if self.account else '未配置')}"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _strip_bearer(value: Optional[str]) -> str:
|
||||
"""去掉 token 字符串可能携带的 'Bearer ' 前缀"""
|
||||
if not value:
|
||||
return ""
|
||||
value = value.strip()
|
||||
return value[7:].strip() if value.lower().startswith("bearer ") else value
|
||||
|
||||
@property
|
||||
def client(self) -> httpx.AsyncClient:
|
||||
@@ -54,56 +89,232 @@ class AgileDBAPIClient:
|
||||
self._client = httpx.AsyncClient(timeout=self.default_timeout)
|
||||
return self._client
|
||||
|
||||
async def _ensure_token(self) -> str:
|
||||
"""确保已有可用 token,没有则登录获取"""
|
||||
if self._token:
|
||||
return self._token
|
||||
return await self._login()
|
||||
|
||||
async def _login(self) -> str:
|
||||
"""换取 token 并缓存(并发安全,用于首次登录)"""
|
||||
async with self._login_lock:
|
||||
# 双重检查:可能在等锁期间已有其它协程完成登录
|
||||
if self._token:
|
||||
return self._token
|
||||
return await self._do_login()
|
||||
|
||||
async def _relogin(self, stale_token: Optional[str]) -> str:
|
||||
"""登录态失效后重新登录(compare-and-swap,并发安全)。
|
||||
|
||||
仅当当前 token 仍是那次失败请求所用的旧 token 时才真正重登;
|
||||
若在等锁期间已有其它协程刷新过 token,则直接复用新 token,
|
||||
避免把别人刚拿到的新 token 抹掉又触发一次多余的重登。
|
||||
"""
|
||||
async with self._login_lock:
|
||||
if self._token != stale_token:
|
||||
# 别的协程已经刷新过 token,直接用新的
|
||||
return self._token or ""
|
||||
self._token = None
|
||||
return await self._do_login()
|
||||
|
||||
async def _do_login(self) -> str:
|
||||
"""实际执行 /login 的逻辑(调用方需自行持有 _login_lock)。"""
|
||||
if not self.account or not self.password:
|
||||
raise Exception(
|
||||
"未配置认证信息:请设置环境变量 API_KEY,"
|
||||
"或同时设置 AGILE_DB_ACCOUNT 和 AGILE_DB_PASSWORD"
|
||||
)
|
||||
|
||||
url = self._build_url(LOGIN_PATH)
|
||||
payload = {
|
||||
"username": self.account,
|
||||
"password": self.password,
|
||||
"loginType": LOGIN_TYPE,
|
||||
}
|
||||
logger.info(f"[登录] POST {url}, username={self.account}, loginType={LOGIN_TYPE}")
|
||||
try:
|
||||
response = await self.client.post(
|
||||
url,
|
||||
headers={"Content-Type": "application/json"},
|
||||
json=payload,
|
||||
)
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f"登录请求超时: {url}")
|
||||
except httpx.RequestError as e:
|
||||
raise Exception(f"登录请求异常: {url}, 错误: {str(e)}")
|
||||
|
||||
is_json, body = self._try_parse_json(response)
|
||||
data = self._handle_response(response, url, is_json, body)
|
||||
# 平台登录响应:token 在顶层 token 字段
|
||||
token = data.get("token") if isinstance(data, dict) else None
|
||||
if not token:
|
||||
raise Exception(f"登录成功但未返回 token,响应: {json.dumps(data, ensure_ascii=False)[:300]}")
|
||||
|
||||
self._token = self._strip_bearer(token)
|
||||
logger.info("[登录] 成功获取 token")
|
||||
return self._token
|
||||
|
||||
@staticmethod
|
||||
def _try_parse_json(response: httpx.Response):
|
||||
"""尝试把响应体解析为 JSON,只解析一次供后续复用。
|
||||
|
||||
Returns:
|
||||
(is_json, data):是 JSON 则 (True, 解析结果);
|
||||
二进制/非 JSON 响应(如 Excel 下载)则 (False, None)。
|
||||
"""
|
||||
try:
|
||||
return True, response.json()
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
return False, None
|
||||
|
||||
@staticmethod
|
||||
def _is_unauthorized(response: httpx.Response, is_json: bool, data: Any) -> bool:
|
||||
"""判断响应是否为登录态失效(基于已解析好的 body,不重复 parse)。
|
||||
|
||||
平台有两种表达 401 的方式,都需识别:
|
||||
1. HTTP 状态码 401;
|
||||
2. HTTP 200 但 body 信封里 code=401(如 {"code":401,"msg":"登录过期,请重新登录"})。
|
||||
"""
|
||||
if response.status_code == 401:
|
||||
return True
|
||||
return is_json and isinstance(data, dict) and data.get("code") == 401
|
||||
|
||||
@staticmethod
|
||||
def _rewind_files(files: Optional[Dict[str, Any]]) -> None:
|
||||
"""把上传用的文件流游标重置到开头。
|
||||
|
||||
401 重试会复用同一个 files 二次发送,而文件流在第一次发送后游标已到末尾,
|
||||
不 rewind 会导致重试上传空内容。支持两种形态:
|
||||
- 直接的文件对象;
|
||||
- (filename, fileobj, content_type) 元组(httpx multipart 常用写法)。
|
||||
"""
|
||||
if not files:
|
||||
return
|
||||
for value in files.values():
|
||||
fileobj = value
|
||||
if isinstance(value, (tuple, list)) and len(value) >= 2:
|
||||
fileobj = value[1]
|
||||
seek = getattr(fileobj, "seek", None)
|
||||
if callable(seek):
|
||||
try:
|
||||
seek(0)
|
||||
except (OSError, ValueError):
|
||||
# 不可重置的流(如已关闭/不支持 seek)静默跳过,交由上传结果反映
|
||||
pass
|
||||
|
||||
def _get_headers(self, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]:
|
||||
"""获取请求头"""
|
||||
headers = {
|
||||
'Authorization': self.api_key if self.api_key.startswith('Bearer ') else f'Bearer {self.api_key}',
|
||||
}
|
||||
headers = {}
|
||||
if self._token:
|
||||
headers['Authorization'] = f'Bearer {self._token}'
|
||||
if extra_headers:
|
||||
headers.update(extra_headers)
|
||||
return headers
|
||||
|
||||
def _build_url(self, path: str) -> str:
|
||||
"""构建完整 URL,自动去掉路径中多余的 /api 前缀"""
|
||||
"""构建完整 URL
|
||||
|
||||
约定:base_url 已包含完整地址(如含 /api 前缀则配在环境变量里),
|
||||
各工具传入的 path 不带 /api 前缀,此处直接拼接。
|
||||
"""
|
||||
if path.startswith('http://') or path.startswith('https://'):
|
||||
return path
|
||||
# 去掉 /api 前缀,因为 base_url 已经包含完整地址
|
||||
clean_path = path
|
||||
if clean_path.startswith('/api'):
|
||||
clean_path = clean_path[4:]
|
||||
return f"{self.base_url}{clean_path}"
|
||||
return f"{self.base_url}{path}"
|
||||
|
||||
def _handle_response(self, response: httpx.Response, url: str) -> Dict[str, Any]:
|
||||
"""统一处理 API 响应"""
|
||||
def _handle_response(
|
||||
self,
|
||||
response: httpx.Response,
|
||||
url: str,
|
||||
is_json: Optional[bool] = None,
|
||||
data: Any = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""统一处理 API 响应
|
||||
|
||||
is_json / data 为调用方已解析好的 body(避免对大响应重复 parse);
|
||||
未传入时此处自行解析一次。
|
||||
"""
|
||||
logger.info(f"[API响应] HTTP {response.status_code}")
|
||||
|
||||
if response.status_code == 204:
|
||||
return {"success": True, "data": None}
|
||||
|
||||
response.raise_for_status()
|
||||
# 复用调用方解析结果;未提供则在此解析一次
|
||||
if is_json is None:
|
||||
is_json, data = self._try_parse_json(response)
|
||||
|
||||
try:
|
||||
data = response.json()
|
||||
except json.JSONDecodeError:
|
||||
# 非 JSON 响应(如文件下载)
|
||||
# 先看 body 再判断状态码。
|
||||
# 平台会用非 2xx 状态码 + body 里的 {code, msg} 返回业务错误,
|
||||
# 若先 raise_for_status() 会在拿到 body 前抛异常,导致真正的 msg 全部丢失。
|
||||
if not is_json:
|
||||
# 非 JSON 响应(如 Excel/文件下载,二进制内容 .json() 会抛 UnicodeDecodeError)
|
||||
response.raise_for_status()
|
||||
return {"success": True, "data": response.content, "raw": True}
|
||||
|
||||
# 检查平台 API 的 {code, msg} 格式
|
||||
# 平台约定:code 为 200 或 0 均表示成功(见数据库模块文档 §1.1)
|
||||
if isinstance(data, dict) and 'code' in data:
|
||||
if data['code'] != 200:
|
||||
if data['code'] not in (0, 200):
|
||||
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)
|
||||
# code 合法即视为业务成功,不再用 HTTP 状态码二次否决
|
||||
return data
|
||||
|
||||
# 无 {code} 信封的响应:回落到 HTTP 状态码判断,
|
||||
# 非 2xx 时把 body 文本带进异常,避免错误细节丢失。
|
||||
if response.status_code >= 400:
|
||||
detail = data if isinstance(data, str) else json.dumps(data, ensure_ascii=False)
|
||||
logger.error(f"[API错误] HTTP {response.status_code}, {detail}")
|
||||
raise Exception(f"HTTP {response.status_code}: {detail}")
|
||||
|
||||
return data
|
||||
|
||||
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""发送 GET 请求"""
|
||||
async def _request(
|
||||
self,
|
||||
method: str,
|
||||
path: str,
|
||||
*,
|
||||
params: Optional[Dict[str, Any]] = None,
|
||||
json_data: Optional[Dict[str, Any]] = None,
|
||||
files: Optional[Dict[str, Any]] = None,
|
||||
extra_headers: Optional[Dict[str, str]] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""统一请求入口:自动注入认证、登录态失效(401)时重登重试一次"""
|
||||
url = self._build_url(path)
|
||||
# 账号密码模式下首次请求前先确保有 token;纯 api_key 模式 _ensure_token 直接返回
|
||||
await self._ensure_token()
|
||||
|
||||
async def _send() -> httpx.Response:
|
||||
headers = self._get_headers(extra_headers)
|
||||
return await self.client.request(
|
||||
method, url, headers=headers, params=params, json=json_data, files=files
|
||||
)
|
||||
|
||||
try:
|
||||
logger.info(f"[API请求] GET {url}")
|
||||
response = await self.client.get(url, headers=self._get_headers(), params=params)
|
||||
return self._handle_response(response, url)
|
||||
logger.info(f"[API请求] {method} {url}")
|
||||
# 记下本次请求所用 token,供 401 时做 compare-and-swap 重登
|
||||
token_used = self._token
|
||||
response = await _send()
|
||||
is_json, body = self._try_parse_json(response)
|
||||
|
||||
# token 失效:仅在账号密码模式下尝试重新登录并重试一次。
|
||||
# 平台可能用 HTTP 401,也可能用 HTTP 200 + body code=401 表达登录过期,
|
||||
# 两者都要识别(见 _is_unauthorized)。
|
||||
if self._is_unauthorized(response, is_json, body) and self.account and self.password:
|
||||
logger.warning("[认证] 收到 401(登录过期),尝试重新登录后重试一次")
|
||||
# CAS 重登:仅当 token 仍是本次用的旧值才真正重登,否则复用别人刚拿到的新 token
|
||||
await self._relogin(token_used)
|
||||
# 重试前把上传文件流游标重置到开头,避免二次发送空内容
|
||||
self._rewind_files(files)
|
||||
response = await _send()
|
||||
is_json, body = self._try_parse_json(response)
|
||||
|
||||
# 重登后仍判定为登录态失效:账号密码本身失效/被禁,给明确报错
|
||||
if self._is_unauthorized(response, is_json, body):
|
||||
logger.error("[认证] 重新登录后仍返回 401")
|
||||
raise Exception("重新登录后仍未通过认证,请检查账号密码是否正确或账号是否被禁用")
|
||||
|
||||
return self._handle_response(response, url, is_json, body)
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f"API 请求超时: {url}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
@@ -111,68 +322,38 @@ class AgileDBAPIClient:
|
||||
except httpx.RequestError as e:
|
||||
raise Exception(f"API 请求异常: {url}, 错误: {str(e)}")
|
||||
|
||||
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""发送 GET 请求"""
|
||||
return await self._request("GET", path, params=params)
|
||||
|
||||
async def post(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""发送 POST 请求"""
|
||||
url = self._build_url(path)
|
||||
try:
|
||||
logger.info(f"[API请求] POST {url}")
|
||||
headers = self._get_headers({'Content-Type': 'application/json'})
|
||||
response = await self.client.post(url, headers=headers, json=json_data, params=params)
|
||||
return self._handle_response(response, url)
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f"API 请求超时: {url}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}")
|
||||
except httpx.RequestError as e:
|
||||
raise Exception(f"API 请求异常: {url}, 错误: {str(e)}")
|
||||
return await self._request(
|
||||
"POST", path, params=params, json_data=json_data,
|
||||
extra_headers={'Content-Type': 'application/json'},
|
||||
)
|
||||
|
||||
async def put(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""发送 PUT 请求"""
|
||||
url = self._build_url(path)
|
||||
try:
|
||||
logger.info(f"[API请求] PUT {url}")
|
||||
headers = self._get_headers({'Content-Type': 'application/json'})
|
||||
response = await self.client.put(url, headers=headers, json=json_data, params=params)
|
||||
return self._handle_response(response, url)
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f"API 请求超时: {url}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}")
|
||||
except httpx.RequestError as e:
|
||||
raise Exception(f"API 请求异常: {url}, 错误: {str(e)}")
|
||||
return await self._request(
|
||||
"PUT", path, params=params, json_data=json_data,
|
||||
extra_headers={'Content-Type': 'application/json'},
|
||||
)
|
||||
|
||||
async def delete(self, path: str, params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""发送 DELETE 请求"""
|
||||
url = self._build_url(path)
|
||||
try:
|
||||
logger.info(f"[API请求] DELETE {url}")
|
||||
headers = self._get_headers()
|
||||
if json_data is not None:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
response = await self.client.delete(url, headers=headers, params=params, json=json_data)
|
||||
return self._handle_response(response, url)
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f"API 请求超时: {url}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}")
|
||||
except httpx.RequestError as e:
|
||||
raise Exception(f"API 请求异常: {url}, 错误: {str(e)}")
|
||||
"""发送 DELETE 请求
|
||||
|
||||
平台部分 DELETE 接口需带 body,统一走通用 request() 处理。
|
||||
"""
|
||||
extra = {'Content-Type': 'application/json'} if json_data is not None else None
|
||||
return await self._request("DELETE", path, params=params, json_data=json_data, extra_headers=extra)
|
||||
|
||||
async def upload(self, path: str, files: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""发送文件上传请求(multipart/form-data)"""
|
||||
url = self._build_url(path)
|
||||
try:
|
||||
logger.info(f"[API请求] UPLOAD {url}")
|
||||
# 文件上传不需要 Content-Type,httpx 会自动设置 multipart/form-data
|
||||
headers = self._get_headers()
|
||||
response = await self.client.post(url, headers=headers, files=files, params=params)
|
||||
return self._handle_response(response, url)
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f"API 请求超时: {url}")
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}")
|
||||
except httpx.RequestError as e:
|
||||
raise Exception(f"API 请求异常: {url}, 错误: {str(e)}")
|
||||
"""发送文件上传请求(multipart/form-data)
|
||||
|
||||
不显式设置 Content-Type,httpx 会根据 files 自动生成 multipart 边界。
|
||||
"""
|
||||
return await self._request("POST", path, params=params, files=files)
|
||||
|
||||
async def close(self):
|
||||
"""关闭 HTTP 客户端"""
|
||||
|
||||
@@ -6,21 +6,28 @@ from typing import Optional
|
||||
|
||||
def get_api_key(default: Optional[str] = None) -> str:
|
||||
"""
|
||||
获取 API 密钥
|
||||
获取 API 密钥(可选)
|
||||
|
||||
优先级:显式配置了 API_KEY 时直接使用;未配置则返回空串,
|
||||
由客户端回落到账号密码登录流程换取 token。
|
||||
|
||||
Args:
|
||||
default: 默认值(可选)
|
||||
|
||||
Returns:
|
||||
str: API 密钥
|
||||
|
||||
Raises:
|
||||
ValueError: 当 API_KEY 未设置且无默认值时
|
||||
str: API 密钥,未配置时为空串
|
||||
"""
|
||||
value = os.environ.get("API_KEY", default or "")
|
||||
if not value:
|
||||
raise ValueError("环境变量 API_KEY 未设置")
|
||||
return value
|
||||
return os.environ.get("API_KEY", default or "")
|
||||
|
||||
|
||||
def get_account(default: Optional[str] = None) -> str:
|
||||
"""获取登录账号(环境变量 AGILE_DB_ACCOUNT)"""
|
||||
return os.environ.get("AGILE_DB_ACCOUNT", default or "")
|
||||
|
||||
|
||||
def get_password(default: Optional[str] = None) -> str:
|
||||
"""获取登录密码(环境变量 AGILE_DB_PASSWORD)"""
|
||||
return os.environ.get("AGILE_DB_PASSWORD", default or "")
|
||||
|
||||
|
||||
def get_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str:
|
||||
@@ -44,7 +51,8 @@ def get_env_config() -> dict:
|
||||
dict: 包含所有配置的字典
|
||||
"""
|
||||
return {
|
||||
"api_key": get_api_key(""),
|
||||
"api_key": os.environ.get("API_KEY", ""),
|
||||
"account": os.environ.get("AGILE_DB_ACCOUNT", ""),
|
||||
"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 | 是 | 目标数据库名 |
|
||||
| tableName | string | 是 | 表名(小写字母+数字+下划线) |
|
||||
| tableComment | string | 否 | 表注释 |
|
||||
@@ -198,33 +198,29 @@
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| tableId | string | 是 | 表 ID |
|
||||
| columns | array | 是 | 字段变更数组 |
|
||||
| columns[].operation | string | 是 | ADD_COLUMN / MODIFY_COLUMN / DROP_COLUMN |
|
||||
| columns[].columnName | string | 是 | 字段名 |
|
||||
| columns[].columnType | string | 否 | 字段类型(ADD/MODIFY) |
|
||||
| columns[].columnLength | int | 否 | 字段长度 |
|
||||
| columns[].isPrimaryKey | bool | 否 | 是否主键 |
|
||||
| columns[].isNullable | bool | 否 | 是否可空 |
|
||||
| columns[].columnComment | string | 否 | 字段注释 |
|
||||
| columns[].defaultValue | string | 否 | 默认值 |
|
||||
| newTableName | string | 否 | 新表名(重命名) |
|
||||
| newTableComment | string | 否 | 新表注释 |
|
||||
| connectionId | string | 是 | 数据源连接 ID(路径参数) |
|
||||
| databaseName | string | 是 | 数据库名 |
|
||||
| tableName | string | 是 | 表名 |
|
||||
| operations | array | 是 | 表结构变更操作数组 |
|
||||
| operations[].operation | string | 是 | ADD_COLUMN / DROP_COLUMN / RENAME_COLUMN / ALTER_COLUMN_TYPE / SET_NOT_NULL / DROP_NOT_NULL / SET_DEFAULT / DROP_DEFAULT |
|
||||
| operations[].column | object | 是 | 列定义(根据 operation 不同包含不同字段,如 columnName/columnType/newColumnName 等) |
|
||||
| tableComment | string | 否 | 新表注释 |
|
||||
|
||||
**返回**:修改结果
|
||||
|
||||
---
|
||||
|
||||
#### 12. `generate_table_by_description`
|
||||
- **用途**:通过自然语言描述让 AI 生成表结构
|
||||
- **用途**:通过自然语言描述让 AI 生成表结构(异步任务)
|
||||
- **对应前端**:CreateBuiltinDataSource.vue AI 生成表结构
|
||||
- **对应 API**:`postGenerateTable(data)` ✅ 已实现 — `POST /api/datasource/connection/generate_table`
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| description | string | 是 | 业务场景描述(至少6个字符) |
|
||||
| requirement | string | 是 | 业务需求描述 |
|
||||
| databaseId | int | 否 | 关联的数据库 ID |
|
||||
|
||||
**返回**:AI 生成的表结构(表名、表注释、字段列表含类型/主键/注释等)
|
||||
**返回**:异步任务信息(taskId、status),需轮询任务状态获取最终生成的表结构
|
||||
|
||||
> **场景示例**:用户说"我需要一个商城系统,管理商品、分类和用户评价",AI 返回完整的表结构设计。
|
||||
|
||||
@@ -389,6 +385,12 @@
|
||||
|
||||
---
|
||||
|
||||
#### 23.5 ~~`revoke_api_key_permissions`~~(已废弃,不提供)
|
||||
- **结论**:权限为「仅追加」模型。真机验证后端 permission 删除接口返回「不支持当前的调用方式」,无法撤销单条已授予权限,故不实现该工具。
|
||||
- **替代方案**:要缩小某密钥的权限范围,只能「删密钥(`delete_api_key`)→ 重建(`create_api_key`)→ 重新授权(`grant_api_key_permissions`)」。
|
||||
|
||||
---
|
||||
|
||||
### 🤖 技能与工具管理 (内置数据源 AI 能力)
|
||||
|
||||
#### 24. `get_skill_by_datasource`
|
||||
@@ -415,17 +417,9 @@
|
||||
|
||||
---
|
||||
|
||||
#### 26. `create_skill`
|
||||
- **用途**:为数据源创建技能
|
||||
- **对应 API**:`postSkillCreateOrGet(data)` ✅ 已实现 — `POST /api/datasource/skill/createOrGet`
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| datasourceId | string | 是 | 数据源 ID |
|
||||
| name | string | 否 | 技能名称(不传则自动生成) |
|
||||
| description | string | 否 | 技能描述 |
|
||||
|
||||
**返回**:技能 ID
|
||||
#### 26. ~~`create_skill`~~(已移除,不再单独暴露)
|
||||
- **结论**:技能(skill)必须挂着工具才有效,平时不会单独创建技能;单独建技能会留下无效的空技能。前端也没有「只建技能」入口。
|
||||
- **替代方案**:把 SQL 沉淀为工具统一用 `add_sql_tool_to_datasource`(见 §29.5),它内部按需调 `skill/createOrGet` 建技能、写配置模板、再 `confirmTools` 建工具,一步到位且保证技能必有工具。底层 `skill/createOrGet` 端点仍被该编排工具内部使用,只是不再作为独立 MCP 工具暴露。
|
||||
|
||||
---
|
||||
|
||||
@@ -437,12 +431,14 @@
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| skillId | string | 是 | 技能 ID |
|
||||
| name | string | 是 | 工具名称 |
|
||||
| businessDescription | string | 是 | 业务描述 |
|
||||
| sqlTemplate | string | 是 | SQL 模板(支持 #{param} 参数占位) |
|
||||
| sqlParams | string | 否 | 参数 JSON Schema(默认空对象) |
|
||||
| resultType | string | 否 | single/list,默认 list |
|
||||
| businessScenario | string | 否 | 业务场景描述 |
|
||||
| tableIds | array | 否 | 关联的表 ID 数组 |
|
||||
| suggestions | array | 是 | SQL 工具建议数组(支持批量) |
|
||||
| suggestions[].name | string | 是 | 工具名称 |
|
||||
| suggestions[].businessDescription | string | 是 | 业务描述 |
|
||||
| suggestions[].sqlTemplate | string | 是 | SQL 模板(支持 #{param} 参数占位) |
|
||||
| suggestions[].sqlParams | string/object | 否 | 参数 JSON Schema(对象会自动序列化为字符串) |
|
||||
| suggestions[].resultType | string | 否 | single/list,默认 list |
|
||||
| suggestions[].businessScenario | string | 否 | 业务场景描述 |
|
||||
|
||||
**返回**:工具创建结果
|
||||
|
||||
@@ -497,9 +493,15 @@
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| connectionId | int | 是 | 数据源 ID |
|
||||
| databaseName | string | 是 | 落库目标库名 |
|
||||
| target | string | 否 | prod/test |
|
||||
| data | object | 是 | 导入数据(含 tableStructure + allData) |
|
||||
|
||||
**data.tableStructure.columns 的关键约束**:
|
||||
- `columns` 定义了「这批数据要写入哪些字段」,**导入时这些列必须对应目标表中真实存在的字段**——列名(`columnName`)要与目标表的实际字段名一致,否则后端按列名拼 INSERT 时会报「查询字段不存在 / 字段名称不正确」。
|
||||
- `allData` 的**首行是列名表头**(= `columns[].columnName`),数据从第 2 行起;每行按 `columns` 顺序给出**全部列**的值(全列宽,不裁剪)。表头列名同样必须是目标表存在的字段。
|
||||
- **导入到已有表时**:前端会用目标表的真实列(`get_table_detail` 返回的 columns)覆盖 AI 识别的列。MCP 调用方应等价处理——**先 `get_table_detail` 拿到目标表真实字段,再让 data 里的 columns / 表头 / 数据与之对齐**,不要直接用 Excel 识别出的、可能与表字段不符的列。
|
||||
|
||||
**返回**:导入结果
|
||||
|
||||
---
|
||||
|
||||
@@ -7,6 +7,8 @@ from lzwcai_mcp_agile_db.server import main
|
||||
import os
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ["API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjJiMDY0YzMzLTBiZWYtNDU0NC04NWY1LTRmNTFiOGMxMmI5NSJ9.Uv599TvlQvlTlwrnZGo3Tl2eLAvM0ldE9vpMI5jHxbTf4_tVSRA60rUNIV4LBiw6pt1r_xIi7aFcTRE2PeN5sg"
|
||||
os.environ["backendBaseUrl"] = "https://dempdemo.lzwcai.com"
|
||||
# 账号密码方式:客户端会在首次请求时自动调用 /login 换取 token
|
||||
os.environ["AGILE_DB_ACCOUNT"] = "yy8z9"
|
||||
os.environ["AGILE_DB_PASSWORD"] = "lzwc@2025."
|
||||
os.environ["backendBaseUrl"] = "http://192.168.2.236:8082/api"
|
||||
main()
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "lzwcai-mcp-agile-db"
|
||||
version = "0.1.3"
|
||||
version = "0.1.17"
|
||||
description = "MCP server for database management platform with 33 tools for datasource, table, data, API key, and skill management"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
1
lzwcai_mcp_agile_db_third/.python-version
Normal file
1
lzwcai_mcp_agile_db_third/.python-version
Normal file
@@ -0,0 +1 @@
|
||||
3.12
|
||||
75
lzwcai_mcp_agile_db_third/README.md
Normal file
75
lzwcai_mcp_agile_db_third/README.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# lzwcai-mcp-agile-db-third
|
||||
|
||||
AgileDB 数据源管理 MCP Server,基于 `API_DOCUMENTATION.md` 将后端数据源/连接/DDL/DML/API 接口封装为 34 个 MCP 工具。
|
||||
|
||||
## 功能概述
|
||||
|
||||
本服务将后端 `datasource` 模块的 API 接口代理为标准 MCP 工具,分为两大类:
|
||||
|
||||
### 1. 数据源配置管理
|
||||
|
||||
- `list_datasource_configs`:查询数据源配置列表
|
||||
- `get_datasource_config`:获取数据源配置详情
|
||||
- `batch_create_datasource_configs`:批量创建数据源配置
|
||||
- `replace_datasource_configs`:全量替换数据源配置
|
||||
- `batch_update_datasource_configs`:批量修改数据源配置
|
||||
- `delete_datasource_configs`:批量删除数据源配置
|
||||
- `test_connection_config`:测试数据库连接
|
||||
- `change_datasource_status`:修改数据源状态
|
||||
- `export_datasource_configs`:导出数据源配置为 Excel
|
||||
|
||||
### 2. 数据库连接实例管理
|
||||
|
||||
- `list_connections` / `get_connection` / `create_connection` / `update_connection` / `delete_connection`:连接实例 CRUD
|
||||
- `test_connection` / `change_connection_status`:连接测试与状态切换
|
||||
- `realtime_structure` / `realtime_databases` / `realtime_tables`:实时查询库表结构
|
||||
- `create_builtin_postgresql` / `update_builtin_database`:内置 PostgreSQL 连接管理
|
||||
- `execute_sql`:执行原生 SQL
|
||||
- `create_database` / `create_table` / `create_database_table` / `alter_database` / `alter_table`:DDL 操作
|
||||
- `generate_table`:AI 生成表结构
|
||||
- `import_document_preview` / `import_document_confirm`:Excel/CSV 文档导入
|
||||
- `builtin_table_data` / `builtin_table_insert` / `builtin_table_update` / `builtin_table_delete`:表数据 CRUD
|
||||
|
||||
## 环境配置
|
||||
|
||||
| 环境变量 | 说明 | 默认值 |
|
||||
|----------|------|--------|
|
||||
| `backendBaseUrl` | 后端 API 基础地址 | `http://lzwcai-demp-corp-manager:8086` |
|
||||
| `datasourceApiKey` | 默认 `X-Datasource-API-Key`,可选 | 空 |
|
||||
| `LOG_LEVEL` | 日志级别 | `INFO` |
|
||||
|
||||
## 安装与运行
|
||||
|
||||
使用 uv:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run python -m lzwcai_mcp_agile_db_third.main
|
||||
```
|
||||
|
||||
或使用 pip:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
.venv\Scripts\python -m pip install mcp httpx
|
||||
.venv\Scripts\python -m lzwcai_mcp_agile_db_third.main
|
||||
```
|
||||
|
||||
安装为命令后:
|
||||
|
||||
```bash
|
||||
lzwcai-mcp-agile-db-third
|
||||
```
|
||||
|
||||
## 使用 mcp CLI
|
||||
|
||||
```bash
|
||||
mcp dev lzwcai_mcp_agile_db_third/main.py
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有写操作(创建/修改/删除)会先落到 `prod` 环境;带 `target` 参数的工具可切换为 `test`
|
||||
- `import_document_preview` 通过本地文件路径上传 Excel/CSV
|
||||
- 执行删除类工具前,调用方应遵循安全确认原则,向用户展示影响范围并二次确认
|
||||
- 日志文件中会对 `password`、`apiKey`、`token`、`secret` 等敏感字段进行脱敏
|
||||
BIN
lzwcai_mcp_agile_db_third/__pycache__/main.cpython-312.pyc
Normal file
BIN
lzwcai_mcp_agile_db_third/__pycache__/main.cpython-312.pyc
Normal file
Binary file not shown.
21
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/.gitignore
vendored
Normal file
21
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/.gitignore
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.egg-info/
|
||||
*.egg
|
||||
|
||||
# Virtual environments
|
||||
.venv/
|
||||
venv/
|
||||
|
||||
# Logs
|
||||
logs/
|
||||
*.log
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,75 @@
|
||||
# lzwcai-mcp-agile-db-third
|
||||
|
||||
AgileDB 数据源管理 MCP Server,基于 `API_DOCUMENTATION.md` 将后端数据源/连接/DDL/DML/API 接口封装为 34 个 MCP 工具。
|
||||
|
||||
## 功能概述
|
||||
|
||||
本服务将后端 `datasource` 模块的 API 接口代理为标准 MCP 工具,分为两大类:
|
||||
|
||||
### 1. 数据源配置管理
|
||||
|
||||
- `list_datasource_configs`:查询数据源配置列表
|
||||
- `get_datasource_config`:获取数据源配置详情
|
||||
- `batch_create_datasource_configs`:批量创建数据源配置
|
||||
- `replace_datasource_configs`:全量替换数据源配置
|
||||
- `batch_update_datasource_configs`:批量修改数据源配置
|
||||
- `delete_datasource_configs`:批量删除数据源配置
|
||||
- `test_connection_config`:测试数据库连接
|
||||
- `change_datasource_status`:修改数据源状态
|
||||
- `export_datasource_configs`:导出数据源配置为 Excel
|
||||
|
||||
### 2. 数据库连接实例管理
|
||||
|
||||
- `list_connections` / `get_connection` / `create_connection` / `update_connection` / `delete_connection`:连接实例 CRUD
|
||||
- `test_connection` / `change_connection_status`:连接测试与状态切换
|
||||
- `realtime_structure` / `realtime_databases` / `realtime_tables`:实时查询库表结构
|
||||
- `create_builtin_postgresql` / `update_builtin_database`:内置 PostgreSQL 连接管理
|
||||
- `execute_sql`:执行原生 SQL
|
||||
- `create_database` / `create_table` / `create_database_table` / `alter_database` / `alter_table`:DDL 操作
|
||||
- `generate_table`:AI 生成表结构
|
||||
- `import_document_preview` / `import_document_confirm`:Excel/CSV 文档导入
|
||||
- `builtin_table_data` / `builtin_table_insert` / `builtin_table_update` / `builtin_table_delete`:表数据 CRUD
|
||||
|
||||
## 环境配置
|
||||
|
||||
| 环境变量 | 说明 | 默认值 |
|
||||
|----------|------|--------|
|
||||
| `backendBaseUrl` | 后端 API 基础地址 | `http://lzwcai-demp-corp-manager:8086` |
|
||||
| `datasourceApiKey` | 默认 `X-Datasource-API-Key`,可选 | 空 |
|
||||
| `LOG_LEVEL` | 日志级别 | `INFO` |
|
||||
|
||||
## 安装与运行
|
||||
|
||||
使用 uv:
|
||||
|
||||
```bash
|
||||
uv sync
|
||||
uv run python -m lzwcai_mcp_agile_db_third.main
|
||||
```
|
||||
|
||||
或使用 pip:
|
||||
|
||||
```bash
|
||||
python -m venv .venv
|
||||
.venv\Scripts\python -m pip install mcp httpx
|
||||
.venv\Scripts\python -m lzwcai_mcp_agile_db_third.main
|
||||
```
|
||||
|
||||
安装为命令后:
|
||||
|
||||
```bash
|
||||
lzwcai-mcp-agile-db-third
|
||||
```
|
||||
|
||||
## 使用 mcp CLI
|
||||
|
||||
```bash
|
||||
mcp dev lzwcai_mcp_agile_db_third/main.py
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 所有写操作(创建/修改/删除)会先落到 `prod` 环境;带 `target` 参数的工具可切换为 `test`
|
||||
- `import_document_preview` 通过本地文件路径上传 Excel/CSV
|
||||
- 执行删除类工具前,调用方应遵循安全确认原则,向用户展示影响范围并二次确认
|
||||
- 日志文件中会对 `password`、`apiKey`、`token`、`secret` 等敏感字段进行脱敏
|
||||
@@ -0,0 +1 @@
|
||||
"""lzwcai_mcp_agile_db_third package."""
|
||||
216
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/main.py
Normal file
216
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/main.py
Normal file
@@ -0,0 +1,216 @@
|
||||
"""MCP server entrypoint for Agile DB third-party datasource APIs."""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
try:
|
||||
from .tools import list_tools, get_tool
|
||||
from .utils import DataSourceAPIClient, get_env_config, get_api_key
|
||||
from .utils.logger_config import logger_config
|
||||
except ImportError:
|
||||
from tools import list_tools, get_tool
|
||||
from utils import DataSourceAPIClient, get_env_config, get_api_key
|
||||
from utils.logger_config import logger_config
|
||||
|
||||
from mcp.server import NotificationOptions, Server
|
||||
from mcp.server.models import InitializationOptions
|
||||
import mcp.types as types
|
||||
|
||||
mcp_logger = logger_config.setup_mcp_logging()
|
||||
|
||||
|
||||
def _text_response(payload: Dict[str, Any]) -> List[types.TextContent]:
|
||||
"""Build a JSON text response."""
|
||||
return [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=json.dumps(payload, ensure_ascii=False, indent=2),
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
def _build_tool(tool_def: Dict[str, Any]) -> types.Tool:
|
||||
"""Build an MCP Tool from a tool definition."""
|
||||
return types.Tool(
|
||||
name=tool_def["name"],
|
||||
description=tool_def["description"],
|
||||
inputSchema=tool_def["inputSchema"],
|
||||
)
|
||||
|
||||
|
||||
server = Server("lzwcai-mcp-agile-db-third")
|
||||
_api_client: Optional[DataSourceAPIClient] = None
|
||||
|
||||
|
||||
def _get_api_client() -> DataSourceAPIClient:
|
||||
"""Get or create the default API client."""
|
||||
global _api_client
|
||||
if _api_client is None:
|
||||
env = get_env_config()
|
||||
_api_client = DataSourceAPIClient(base_url=env.get("backend_base_url"))
|
||||
return _api_client
|
||||
|
||||
|
||||
@server.list_tools()
|
||||
async def handle_list_tools() -> List[types.Tool]:
|
||||
"""List all available MCP tools."""
|
||||
try:
|
||||
mcp_logger.info("收到列出工具请求")
|
||||
tools = [_build_tool(tool_def) for tool_def in list_tools()]
|
||||
mcp_logger.info(f"成功生成 {len(tools)} 个 MCP 工具")
|
||||
return tools
|
||||
except Exception as e:
|
||||
mcp_logger.error(f"列出工具失败: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(
|
||||
name: str,
|
||||
arguments: Optional[Dict[str, Any]],
|
||||
) -> List[types.TextContent]:
|
||||
"""Handle MCP tool invocation by proxying to the backend API."""
|
||||
try:
|
||||
mcp_logger.info(f"收到工具调用请求: {name}")
|
||||
mcp_logger.debug(f"工具参数: {arguments}")
|
||||
|
||||
tool_def = get_tool(name)
|
||||
if tool_def is None:
|
||||
error_msg = f"未找到工具: {name}"
|
||||
mcp_logger.warning(error_msg)
|
||||
return _text_response({"error": error_msg})
|
||||
|
||||
# Validate required parameters
|
||||
args = arguments or {}
|
||||
schema = tool_def["inputSchema"]
|
||||
required = schema.get("required", [])
|
||||
missing = [key for key in required if key not in args or args[key] is None]
|
||||
if missing:
|
||||
error_msg = f"工具 {name} 缺少必填参数: {', '.join(missing)}"
|
||||
mcp_logger.warning(error_msg)
|
||||
return _text_response({"error": error_msg, "missing": missing})
|
||||
|
||||
# Categorize parameters
|
||||
path_params: Dict[str, Any] = {}
|
||||
query_params: Dict[str, Any] = {}
|
||||
body_params: Dict[str, Any] = {}
|
||||
file_path: Optional[str] = None
|
||||
api_key: Optional[str] = None
|
||||
|
||||
categories = tool_def["paramCategories"]
|
||||
for key, value in args.items():
|
||||
if value is None:
|
||||
continue
|
||||
category = categories.get(key)
|
||||
if category == "path":
|
||||
path_params[key] = value
|
||||
elif category == "query":
|
||||
query_params[key] = value
|
||||
elif category == "body":
|
||||
body_params[key] = value
|
||||
elif category == "file":
|
||||
file_path = value
|
||||
elif category == "header":
|
||||
api_key = value
|
||||
|
||||
# Apply schema-defined defaults for parameters the caller omitted,
|
||||
# so "有默认值的参数不填就用默认值"(如 datasourceType、target、分页)。
|
||||
properties = schema.get("properties", {})
|
||||
for key, prop in properties.items():
|
||||
if "default" not in prop:
|
||||
continue
|
||||
if args.get(key) is not None:
|
||||
continue
|
||||
default_value = prop["default"]
|
||||
category = categories.get(key)
|
||||
if category == "path":
|
||||
path_params[key] = default_value
|
||||
elif category == "query":
|
||||
query_params[key] = default_value
|
||||
elif category == "body":
|
||||
body_params[key] = default_value
|
||||
elif category == "header" and not api_key:
|
||||
api_key = default_value
|
||||
|
||||
# Fall back to the environment-configured API key when the tool call
|
||||
# doesn't carry an explicit header value.
|
||||
if not api_key:
|
||||
api_key = get_api_key() or None
|
||||
if api_key:
|
||||
mcp_logger.info("工具调用未带 API Key,已回退到环境变量 datasourceApiKey")
|
||||
|
||||
client = _get_api_client()
|
||||
result = client.request(
|
||||
method=tool_def["method"],
|
||||
path_template=tool_def["path"],
|
||||
path_params=path_params,
|
||||
query_params=query_params,
|
||||
body=body_params if body_params else None,
|
||||
file_path=file_path,
|
||||
api_key=api_key,
|
||||
)
|
||||
|
||||
return _text_response(result)
|
||||
except Exception as e:
|
||||
error_msg = f"工具调用失败: {name}, 错误: {e}"
|
||||
mcp_logger.error(error_msg, exc_info=True)
|
||||
return _text_response({"error": error_msg})
|
||||
|
||||
|
||||
async def async_main():
|
||||
"""Async entry for the MCP server."""
|
||||
try:
|
||||
mcp_logger.info("=" * 60)
|
||||
mcp_logger.info("正在启动 MCP 服务: lzwcai-mcp-agile-db-third")
|
||||
mcp_logger.info("版本: 0.1.0")
|
||||
mcp_logger.info("=" * 60)
|
||||
|
||||
env = get_env_config()
|
||||
mcp_logger.info(f"环境配置 - Backend Base URL: {env.get('backend_base_url')}")
|
||||
mcp_logger.info(f"环境配置 - API Key: {'已设置' if env.get('api_key') else '未设置'}")
|
||||
mcp_logger.info("=" * 60)
|
||||
|
||||
from mcp.server.stdio import stdio_server
|
||||
|
||||
async with stdio_server() as (read_stream, write_stream):
|
||||
mcp_logger.info("MCP 服务已启动,等待客户端连接...")
|
||||
await server.run(
|
||||
read_stream,
|
||||
write_stream,
|
||||
InitializationOptions(
|
||||
server_name="lzwcai-mcp-agile-db-third",
|
||||
server_version="0.1.2",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
mcp_logger.info("MCP 服务已关闭")
|
||||
except Exception as e:
|
||||
mcp_logger.error(f"MCP 服务运行失败: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
def main():
|
||||
"""Console entrypoint."""
|
||||
try:
|
||||
logger_config.setup_logging(
|
||||
app_name="lzwcai_mcp_agile_db_third",
|
||||
log_level=logging.INFO,
|
||||
console_output=False,
|
||||
)
|
||||
mcp_logger.info("开始运行 MCP Agile DB Third 服务")
|
||||
asyncio.run(async_main())
|
||||
except KeyboardInterrupt:
|
||||
mcp_logger.info("收到中断信号,正在关闭服务...")
|
||||
except Exception as e:
|
||||
mcp_logger.error(f"程序运行失败: {e}", exc_info=True)
|
||||
raise
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
137
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/mock_db.py
Normal file
137
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/mock_db.py
Normal file
@@ -0,0 +1,137 @@
|
||||
"""Docker-based PostgreSQL mock for local self-testing.
|
||||
|
||||
Provides a throwaway Postgres container that the backend can connect to during
|
||||
external-connection tests. The container is bound to 0.0.0.0:5432 on the Docker
|
||||
host, so the host IP must be reachable from the backend server.
|
||||
"""
|
||||
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
DEFAULT_IMAGE = "postgres:16-alpine"
|
||||
DEFAULT_CONTAINER = "lzwcai_mcp_agile_db_third_mock_pg"
|
||||
DEFAULT_PORT = 5432
|
||||
DEFAULT_DATABASE = "postgres"
|
||||
DEFAULT_USERNAME = "postgres"
|
||||
DEFAULT_PASSWORD = "postgres"
|
||||
|
||||
|
||||
def _run(args: list, check: bool = False, capture: bool = True) -> subprocess.CompletedProcess:
|
||||
"""Run a shell command and return the result."""
|
||||
return subprocess.run(
|
||||
args,
|
||||
check=check,
|
||||
capture_output=capture,
|
||||
text=True,
|
||||
shell=False,
|
||||
)
|
||||
|
||||
|
||||
def host_ip(prefer_prefixes: Optional[list] = None) -> str:
|
||||
"""Return a non-loopback IPv4 address of this machine.
|
||||
|
||||
Prefers addresses starting with the given prefixes so users can steer the
|
||||
result toward the network segment that the backend can reach.
|
||||
"""
|
||||
prefer_prefixes = prefer_prefixes or ["192.168.", "10.", "172."]
|
||||
hostname = socket.gethostname()
|
||||
_, _, ips = socket.gethostbyname_ex(hostname)
|
||||
# Prefer addresses matching a requested prefix, then any non-loopback.
|
||||
for prefix in prefer_prefixes:
|
||||
for ip in ips:
|
||||
if ip.startswith(prefix) and not ip.startswith("127."):
|
||||
return ip
|
||||
for ip in ips:
|
||||
if not ip.startswith("127."):
|
||||
return ip
|
||||
raise RuntimeError("Could not find a non-loopback IPv4 address on this host")
|
||||
|
||||
|
||||
def is_running(container: str = DEFAULT_CONTAINER) -> bool:
|
||||
"""Check whether the mock container is currently running."""
|
||||
result = _run(["docker", "ps", "--filter", f"name={container}", "--format", "{{.Names}}"])
|
||||
return container in result.stdout
|
||||
|
||||
|
||||
def start(
|
||||
container: str = DEFAULT_CONTAINER,
|
||||
port: int = DEFAULT_PORT,
|
||||
username: str = DEFAULT_USERNAME,
|
||||
password: str = DEFAULT_PASSWORD,
|
||||
database: str = DEFAULT_DATABASE,
|
||||
image: str = DEFAULT_IMAGE,
|
||||
) -> str:
|
||||
"""Start the Postgres mock container if not already running.
|
||||
|
||||
Returns the host IP that should be supplied to backend connection tests.
|
||||
"""
|
||||
if is_running(container):
|
||||
print(f"Mock DB container '{container}' is already running.")
|
||||
return host_ip()
|
||||
|
||||
# Remove any stale container with the same name.
|
||||
_run(["docker", "rm", "-f", container], check=False)
|
||||
|
||||
print(f"Starting mock Postgres container '{container}' (image={image})...")
|
||||
_run(
|
||||
[
|
||||
"docker", "run", "-d",
|
||||
"--name", container,
|
||||
"-p", f"{port}:{port}",
|
||||
"-e", f"POSTGRES_USER={username}",
|
||||
"-e", f"POSTGRES_PASSWORD={password}",
|
||||
"-e", f"POSTGRES_DB={database}",
|
||||
image,
|
||||
],
|
||||
check=True,
|
||||
)
|
||||
|
||||
print("Waiting for Postgres to become ready...")
|
||||
deadline = time.time() + 30
|
||||
while time.time() < deadline:
|
||||
result = _run(
|
||||
["docker", "exec", container, "pg_isready", "-U", username],
|
||||
check=False,
|
||||
)
|
||||
if result.returncode == 0:
|
||||
break
|
||||
time.sleep(0.5)
|
||||
else:
|
||||
stop(container)
|
||||
raise RuntimeError("Postgres mock failed to become ready within 30s")
|
||||
|
||||
ip = host_ip()
|
||||
print(f"Mock Postgres is ready at {ip}:{port} (user={username}, password={password}, db={database})")
|
||||
return ip
|
||||
|
||||
|
||||
def stop(container: str = DEFAULT_CONTAINER) -> None:
|
||||
"""Stop and remove the mock Postgres container."""
|
||||
print(f"Stopping mock DB container '{container}'...")
|
||||
_run(["docker", "stop", "-t", "5", container], check=False)
|
||||
_run(["docker", "rm", "-f", container], check=False)
|
||||
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python -m lzwcai_mcp_agile_db_third.mock_db [start|stop|ip]")
|
||||
sys.exit(1)
|
||||
cmd = sys.argv[1].lower()
|
||||
if cmd == "start":
|
||||
start()
|
||||
elif cmd == "stop":
|
||||
stop()
|
||||
elif cmd in ("ip", "host"):
|
||||
print(host_ip())
|
||||
elif cmd == "status":
|
||||
print("running" if is_running() else "stopped")
|
||||
else:
|
||||
print(f"Unknown command: {cmd}")
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "lzwcai-mcp-agile-db-third"
|
||||
version = "0.1.3"
|
||||
description = "MCP server for Agile DB third-party datasource APIs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "lzwcai", email = "your-email@example.com"},
|
||||
]
|
||||
keywords = ["mcp", "datasource", "database", "agile-db"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
dependencies = [
|
||||
"httpx>=0.28.1",
|
||||
"mcp[cli]>=1.10.1",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
lzwcai-mcp-agile-db-third = "lzwcai_mcp_agile_db_third.main:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["lzwcai_mcp_agile_db_third"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.force-include]
|
||||
|
||||
[tool.hatch.build]
|
||||
exclude = [
|
||||
"lzwcai_mcp_agile_db_third/logs/**",
|
||||
"**/.__pycache__/**",
|
||||
"lzwcai_mcp_agile_db_third/.gitignore",
|
||||
".venv/**",
|
||||
]
|
||||
870
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/tools.py
Normal file
870
lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/tools.py
Normal file
@@ -0,0 +1,870 @@
|
||||
"""MCP tool definitions for datasource API."""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
|
||||
def _tool(
|
||||
name: str,
|
||||
description: str,
|
||||
method: str,
|
||||
path: str,
|
||||
properties: Dict[str, Any],
|
||||
required: List[str],
|
||||
param_categories: Dict[str, str],
|
||||
) -> Dict[str, Any]:
|
||||
"""Build a tool definition dictionary."""
|
||||
return {
|
||||
"name": name,
|
||||
"description": description,
|
||||
"method": method,
|
||||
"path": path,
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
},
|
||||
"paramCategories": param_categories,
|
||||
}
|
||||
|
||||
|
||||
# 公共参数 schema 片段
|
||||
_API_KEY_PROP = {
|
||||
"apiKey": {
|
||||
"type": "string",
|
||||
"description": "可选的 X-Datasource-API-Key 权限密钥",
|
||||
}
|
||||
}
|
||||
|
||||
_TARGET_PROP = {
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "目标环境(prod/test,默认 prod)",
|
||||
"default": "prod",
|
||||
}
|
||||
}
|
||||
|
||||
_PAGE_PROPS = {
|
||||
"pageNum": {"type": "integer", "description": "页码(默认1)", "default": 1},
|
||||
"pageSize": {"type": "integer", "description": "每页数量(默认10)", "default": 10},
|
||||
}
|
||||
|
||||
# =============================================================================
|
||||
# 1. 数据源配置管理
|
||||
# =============================================================================
|
||||
|
||||
LIST_DATASOURCE_CONFIGS = _tool(
|
||||
name="list_datasource_configs",
|
||||
description="查询数据源配置列表,支持分页和多条件筛选",
|
||||
method="GET",
|
||||
path="/datasource/config/list",
|
||||
properties={
|
||||
"datasourceName": {"type": "string", "description": "数据源名称(模糊查询)"},
|
||||
"datasourceId": {"type": "integer", "description": "连接ID"},
|
||||
"datasourceType": {"type": "string", "description": "数据库类型"},
|
||||
"status": {"type": "integer", "description": "状态"},
|
||||
"showTable": {"type": "boolean", "description": "是否显示表数量"},
|
||||
**_PAGE_PROPS,
|
||||
},
|
||||
required=[],
|
||||
param_categories={
|
||||
"datasourceName": "query",
|
||||
"datasourceId": "query",
|
||||
"datasourceType": "query",
|
||||
"status": "query",
|
||||
"showTable": "query",
|
||||
"pageNum": "query",
|
||||
"pageSize": "query",
|
||||
},
|
||||
)
|
||||
|
||||
GET_DATASOURCE_CONFIG = _tool(
|
||||
name="get_datasource_config",
|
||||
description="获取指定数据源配置的详细信息",
|
||||
method="GET",
|
||||
path="/datasource/config/{id}",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "数据源配置ID"},
|
||||
},
|
||||
required=["id"],
|
||||
param_categories={"id": "path"},
|
||||
)
|
||||
|
||||
BATCH_CREATE_DATASOURCE_CONFIGS = _tool(
|
||||
name="batch_create_datasource_configs",
|
||||
description="批量创建数据源配置并同步表结构",
|
||||
method="POST",
|
||||
path="/datasource/config",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"datasourceNamePrefix": {"type": "string", "description": "数据源名称前缀"},
|
||||
"enterpriseId": {"type": "integer", "description": "企业ID"},
|
||||
"status": {"type": "integer", "description": "状态"},
|
||||
"remark": {"type": "string", "description": "备注信息"},
|
||||
"syncTables": {"type": "boolean", "description": "是否同步表结构"},
|
||||
"databases": {
|
||||
"type": "array",
|
||||
"description": "数据库与表列表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"databaseName": {"type": "string"},
|
||||
"tableNames": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required=["connectionId", "databases"],
|
||||
param_categories={
|
||||
"connectionId": "body",
|
||||
"datasourceNamePrefix": "body",
|
||||
"enterpriseId": "body",
|
||||
"status": "body",
|
||||
"remark": "body",
|
||||
"syncTables": "body",
|
||||
"databases": "body",
|
||||
},
|
||||
)
|
||||
|
||||
REPLACE_DATASOURCE_CONFIGS = _tool(
|
||||
name="replace_datasource_configs",
|
||||
description="全量替换指定连接下的数据源配置(未传入的配置将被删除),请谨慎操作",
|
||||
method="PUT",
|
||||
path="/datasource/config",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"datasourceNamePrefix": {"type": "string", "description": "数据源名称前缀"},
|
||||
"enterpriseId": {"type": "integer", "description": "企业ID"},
|
||||
"status": {"type": "integer", "description": "状态"},
|
||||
"remark": {"type": "string", "description": "备注信息"},
|
||||
"syncTables": {"type": "boolean", "description": "是否同步表结构"},
|
||||
"databases": {
|
||||
"type": "array",
|
||||
"description": "数据库与表列表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"databaseName": {"type": "string"},
|
||||
"tableNames": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required=["connectionId", "databases"],
|
||||
param_categories={
|
||||
"connectionId": "body",
|
||||
"datasourceNamePrefix": "body",
|
||||
"enterpriseId": "body",
|
||||
"status": "body",
|
||||
"remark": "body",
|
||||
"syncTables": "body",
|
||||
"databases": "body",
|
||||
},
|
||||
)
|
||||
|
||||
BATCH_UPDATE_DATASOURCE_CONFIGS = _tool(
|
||||
name="batch_update_datasource_configs",
|
||||
description="批量修改数据源配置并重新同步表结构",
|
||||
method="PUT",
|
||||
path="/datasource/config/batch",
|
||||
properties={
|
||||
"status": {"type": "integer", "description": "状态"},
|
||||
"remark": {"type": "string", "description": "批量更新备注"},
|
||||
"syncTables": {"type": "boolean", "description": "是否同步表结构"},
|
||||
"datasources": {
|
||||
"type": "array",
|
||||
"description": "要更新的数据源配置列表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "integer"},
|
||||
"datasourceName": {"type": "string"},
|
||||
"tableNames": {"type": "array", "items": {"type": "string"}},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
required=["datasources"],
|
||||
param_categories={
|
||||
"status": "body",
|
||||
"remark": "body",
|
||||
"syncTables": "body",
|
||||
"datasources": "body",
|
||||
},
|
||||
)
|
||||
|
||||
DELETE_DATASOURCE_CONFIGS = _tool(
|
||||
name="delete_datasource_configs",
|
||||
description="批量删除数据源配置",
|
||||
method="POST",
|
||||
path="/datasource/config/deletes",
|
||||
properties={
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"description": "要删除的数据源配置ID列表",
|
||||
"items": {"type": "integer"},
|
||||
},
|
||||
},
|
||||
required=["ids"],
|
||||
param_categories={"ids": "body"},
|
||||
)
|
||||
|
||||
TEST_CONNECTION_CONFIG = _tool(
|
||||
name="test_connection_config",
|
||||
description="测试数据库连接是否正常",
|
||||
method="POST",
|
||||
path="/datasource/config/testConnectionConfig",
|
||||
properties={
|
||||
"host": {"type": "string", "description": "主机地址"},
|
||||
"port": {"type": "integer", "description": "端口"},
|
||||
"username": {"type": "string", "description": "用户名"},
|
||||
"password": {"type": "string", "description": "密码"},
|
||||
"datasourceType": {"type": "string", "description": "数据库类型,例如 PostgreSQL(默认 PostgreSQL)", "default": "PostgreSQL"},
|
||||
},
|
||||
required=["host", "port", "username", "password"],
|
||||
param_categories={
|
||||
"host": "body",
|
||||
"port": "body",
|
||||
"username": "body",
|
||||
"password": "body",
|
||||
"datasourceType": "body",
|
||||
},
|
||||
)
|
||||
|
||||
CHANGE_DATASOURCE_STATUS = _tool(
|
||||
name="change_datasource_status",
|
||||
description="修改数据源配置的启用/禁用状态",
|
||||
method="PUT",
|
||||
path="/datasource/config/changeStatus",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "数据源配置ID"},
|
||||
"status": {"type": "integer", "description": "状态(0正常/1停用)"},
|
||||
},
|
||||
required=["id", "status"],
|
||||
param_categories={"id": "body", "status": "body"},
|
||||
)
|
||||
|
||||
EXPORT_DATASOURCE_CONFIGS = _tool(
|
||||
name="export_datasource_configs",
|
||||
description="导出数据源配置列表为Excel文件",
|
||||
method="POST",
|
||||
path="/datasource/config/export",
|
||||
properties={
|
||||
"datasourceName": {"type": "string", "description": "数据源名称(模糊查询)"},
|
||||
"datasourceId": {"type": "integer", "description": "连接ID"},
|
||||
"datasourceType": {"type": "string", "description": "数据库类型"},
|
||||
"status": {"type": "integer", "description": "状态"},
|
||||
},
|
||||
required=[],
|
||||
param_categories={
|
||||
"datasourceName": "query",
|
||||
"datasourceId": "query",
|
||||
"datasourceType": "query",
|
||||
"status": "query",
|
||||
},
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
# 2. 数据库连接实例管理
|
||||
# =============================================================================
|
||||
|
||||
LIST_CONNECTIONS = _tool(
|
||||
name="list_connections",
|
||||
description="查询数据库连接实例列表,支持分页和筛选",
|
||||
method="GET",
|
||||
path="/datasource/connection/list",
|
||||
properties={
|
||||
"datasourceName": {"type": "string", "description": "数据源名称(模糊查询)"},
|
||||
"status": {"type": "integer", "description": "状态"},
|
||||
"testStatus": {"type": "integer", "description": "测试状态(0未测试/1成功/2失败)"},
|
||||
"sourceType": {"type": "string", "description": "连接来源(builtin/external)"},
|
||||
**_PAGE_PROPS,
|
||||
},
|
||||
required=[],
|
||||
param_categories={
|
||||
"datasourceName": "query",
|
||||
"status": "query",
|
||||
"testStatus": "query",
|
||||
"sourceType": "query",
|
||||
"pageNum": "query",
|
||||
"pageSize": "query",
|
||||
},
|
||||
)
|
||||
|
||||
GET_CONNECTION = _tool(
|
||||
name="get_connection",
|
||||
description="获取指定连接实例的详细信息",
|
||||
method="GET",
|
||||
path="/datasource/connection/{id}",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "连接实例ID"},
|
||||
},
|
||||
required=["id"],
|
||||
param_categories={"id": "path"},
|
||||
)
|
||||
|
||||
CREATE_CONNECTION = _tool(
|
||||
name="create_connection",
|
||||
description="创建新的数据库连接实例",
|
||||
method="POST",
|
||||
path="/datasource/connection",
|
||||
properties={
|
||||
"datasourceName": {"type": "string", "description": "连接名称"},
|
||||
"datasourceType": {"type": "string", "description": "数据库类型,例如 PostgreSQL(默认 PostgreSQL)", "default": "PostgreSQL"},
|
||||
"connectionType": {"type": "string", "description": "连接方式,例如 user_password(默认 user_password)", "default": "user_password"},
|
||||
"host": {"type": "string", "description": "主机地址"},
|
||||
"port": {"type": "integer", "description": "端口"},
|
||||
"username": {"type": "string", "description": "用户名"},
|
||||
"password": {"type": "string", "description": "密码"},
|
||||
"status": {"type": "integer", "description": "状态"},
|
||||
"remark": {"type": "string", "description": "备注信息"},
|
||||
},
|
||||
required=["datasourceName", "host", "port", "username", "password"],
|
||||
param_categories={
|
||||
"datasourceName": "body",
|
||||
"datasourceType": "body",
|
||||
"connectionType": "body",
|
||||
"host": "body",
|
||||
"port": "body",
|
||||
"username": "body",
|
||||
"password": "body",
|
||||
"status": "body",
|
||||
"remark": "body",
|
||||
},
|
||||
)
|
||||
|
||||
UPDATE_CONNECTION = _tool(
|
||||
name="update_connection",
|
||||
description="修改数据库连接实例信息",
|
||||
method="PUT",
|
||||
path="/datasource/connection",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "连接实例ID"},
|
||||
"datasourceName": {"type": "string", "description": "连接名称"},
|
||||
"host": {"type": "string", "description": "主机地址"},
|
||||
"port": {"type": "integer", "description": "端口"},
|
||||
"username": {"type": "string", "description": "用户名"},
|
||||
"password": {"type": "string", "description": "密码"},
|
||||
"remark": {"type": "string", "description": "更新备注"},
|
||||
},
|
||||
required=["id"],
|
||||
param_categories={
|
||||
"id": "body",
|
||||
"datasourceName": "body",
|
||||
"host": "body",
|
||||
"port": "body",
|
||||
"username": "body",
|
||||
"password": "body",
|
||||
"remark": "body",
|
||||
},
|
||||
)
|
||||
|
||||
DELETE_CONNECTION = _tool(
|
||||
name="delete_connection",
|
||||
description="删除指定的连接实例",
|
||||
method="DELETE",
|
||||
path="/datasource/connection/{id}",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "连接实例ID"},
|
||||
},
|
||||
required=["id"],
|
||||
param_categories={"id": "path"},
|
||||
)
|
||||
|
||||
TEST_CONNECTION = _tool(
|
||||
name="test_connection",
|
||||
description="测试数据库连接是否可用",
|
||||
method="POST",
|
||||
path="/datasource/connection/test",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "连接实例ID"},
|
||||
"host": {"type": "string", "description": "主机地址"},
|
||||
"port": {"type": "integer", "description": "端口"},
|
||||
"username": {"type": "string", "description": "用户名"},
|
||||
"password": {"type": "string", "description": "密码"},
|
||||
"datasourceType": {"type": "string", "description": "数据库类型,例如 PostgreSQL(默认 PostgreSQL)", "default": "PostgreSQL"},
|
||||
},
|
||||
required=["host", "port", "username", "password"],
|
||||
param_categories={
|
||||
"id": "body",
|
||||
"host": "body",
|
||||
"port": "body",
|
||||
"username": "body",
|
||||
"password": "body",
|
||||
"datasourceType": "body",
|
||||
},
|
||||
)
|
||||
|
||||
CHANGE_CONNECTION_STATUS = _tool(
|
||||
name="change_connection_status",
|
||||
description="修改连接实例的启用/禁用状态",
|
||||
method="PUT",
|
||||
path="/datasource/connection/changeStatus",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "连接实例ID"},
|
||||
"status": {"type": "integer", "description": "状态(0正常/1停用)"},
|
||||
},
|
||||
required=["id", "status"],
|
||||
param_categories={"id": "body", "status": "body"},
|
||||
)
|
||||
|
||||
REALTIME_STRUCTURE = _tool(
|
||||
name="realtime_structure",
|
||||
description="实时查询连接下的所有数据库和表结构(直接连接数据库服务器查询)",
|
||||
method="GET",
|
||||
path="/datasource/connection/realtime/structure/{id}",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "连接实例ID"},
|
||||
},
|
||||
required=["id"],
|
||||
param_categories={"id": "path"},
|
||||
)
|
||||
|
||||
REALTIME_DATABASES = _tool(
|
||||
name="realtime_databases",
|
||||
description="实时查询连接下的所有数据库名称",
|
||||
method="GET",
|
||||
path="/datasource/connection/realtime/databases/{id}",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "连接实例ID"},
|
||||
},
|
||||
required=["id"],
|
||||
param_categories={"id": "path"},
|
||||
)
|
||||
|
||||
REALTIME_TABLES = _tool(
|
||||
name="realtime_tables",
|
||||
description="实时查询指定数据库下的所有表",
|
||||
method="GET",
|
||||
path="/datasource/connection/realtime/tables/{id}",
|
||||
properties={
|
||||
"id": {"type": "integer", "description": "连接实例ID"},
|
||||
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||
},
|
||||
required=["id", "databaseName"],
|
||||
param_categories={"id": "path", "databaseName": "query"},
|
||||
)
|
||||
|
||||
CREATE_BUILTIN_POSTGRESQL = _tool(
|
||||
name="create_builtin_postgresql",
|
||||
description="创建内置 PostgreSQL 数据库连接(使用配置文件中的连接信息)",
|
||||
method="POST",
|
||||
path="/datasource/connection/create_builtin_postgresql",
|
||||
properties={
|
||||
"datasourceName": {"type": "string", "description": "连接名称"},
|
||||
"remark": {"type": "string", "description": "备注"},
|
||||
},
|
||||
required=["datasourceName"],
|
||||
param_categories={"datasourceName": "body", "remark": "body"},
|
||||
)
|
||||
|
||||
UPDATE_BUILTIN_DATABASE = _tool(
|
||||
name="update_builtin_database",
|
||||
description="修改内置 PostgreSQL 连接信息",
|
||||
method="PUT",
|
||||
path="/datasource/connection/update_builtin_database",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"datasourceName": {"type": "string", "description": "新名称"},
|
||||
"remark": {"type": "string", "description": "更新备注"},
|
||||
},
|
||||
required=["connectionId"],
|
||||
param_categories={"connectionId": "body", "datasourceName": "body", "remark": "body"},
|
||||
)
|
||||
|
||||
EXECUTE_SQL = _tool(
|
||||
name="execute_sql",
|
||||
description="在指定数据源上执行 SQL 语句,支持参数化查询和环境切换",
|
||||
method="POST",
|
||||
path="/datasource/connection/{datasourceId}/execute_sql",
|
||||
properties={
|
||||
"datasourceId": {"type": "integer", "description": "数据源配置ID"},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "目标环境(prod/test,默认 prod)",
|
||||
"default": "prod",
|
||||
},
|
||||
"sql": {"type": "string", "description": "SQL 语句,使用 ? 占位符"},
|
||||
"params": {
|
||||
"type": "array",
|
||||
"description": "SQL 参数列表",
|
||||
"items": {},
|
||||
},
|
||||
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||
**_API_KEY_PROP,
|
||||
},
|
||||
required=["datasourceId", "sql"],
|
||||
param_categories={
|
||||
"datasourceId": "path",
|
||||
"target": "query",
|
||||
"sql": "body",
|
||||
"params": "body",
|
||||
"databaseName": "body",
|
||||
"apiKey": "header",
|
||||
},
|
||||
)
|
||||
|
||||
CREATE_DATABASE = _tool(
|
||||
name="create_database",
|
||||
description="在指定连接上创建新数据库(目前仅支持 PostgreSQL)",
|
||||
method="POST",
|
||||
path="/datasource/connection/{connectionId}/create_database",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||
"encoding": {"type": "string", "description": "字符编码,例如 UTF8"},
|
||||
"owner": {"type": "string", "description": "所有者"},
|
||||
**_API_KEY_PROP,
|
||||
},
|
||||
required=["connectionId", "databaseName"],
|
||||
param_categories={
|
||||
"connectionId": "path",
|
||||
"databaseName": "body",
|
||||
"encoding": "body",
|
||||
"owner": "body",
|
||||
"apiKey": "header",
|
||||
},
|
||||
)
|
||||
|
||||
CREATE_TABLE = _tool(
|
||||
name="create_table",
|
||||
description="在指定数据库中创建新表",
|
||||
method="POST",
|
||||
path="/datasource/connection/{connectionId}/create_table",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||
"tableName": {"type": "string", "description": "表名"},
|
||||
"tableComment": {"type": "string", "description": "表注释"},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"description": "列定义列表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"columnName": {"type": "string"},
|
||||
"columnType": {"type": "string"},
|
||||
"columnLength": {"type": "integer"},
|
||||
"isPrimaryKey": {"type": "boolean"},
|
||||
"isNullable": {"type": "boolean"},
|
||||
"columnComment": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
**_API_KEY_PROP,
|
||||
},
|
||||
required=["connectionId", "databaseName", "tableName", "columns"],
|
||||
param_categories={
|
||||
"connectionId": "path",
|
||||
"databaseName": "body",
|
||||
"tableName": "body",
|
||||
"tableComment": "body",
|
||||
"columns": "body",
|
||||
"apiKey": "header",
|
||||
},
|
||||
)
|
||||
|
||||
CREATE_DATABASE_TABLE = _tool(
|
||||
name="create_database_table",
|
||||
description="同时创建数据库和表(一次性操作)",
|
||||
method="POST",
|
||||
path="/datasource/connection/{connectionId}/create_database_table",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||
"encoding": {"type": "string", "description": "字符编码"},
|
||||
"owner": {"type": "string", "description": "所有者"},
|
||||
"tables": {
|
||||
"type": "array",
|
||||
"description": "要创建的表列表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableName": {"type": "string"},
|
||||
"tableComment": {"type": "string"},
|
||||
"columns": {"type": "array"},
|
||||
},
|
||||
},
|
||||
},
|
||||
**_API_KEY_PROP,
|
||||
},
|
||||
required=["connectionId", "databaseName", "tables"],
|
||||
param_categories={
|
||||
"connectionId": "path",
|
||||
"databaseName": "body",
|
||||
"encoding": "body",
|
||||
"owner": "body",
|
||||
"tables": "body",
|
||||
"apiKey": "header",
|
||||
},
|
||||
)
|
||||
|
||||
ALTER_DATABASE = _tool(
|
||||
name="alter_database",
|
||||
description="修改数据库属性(重命名、更改所有者、更改编码)",
|
||||
method="PUT",
|
||||
path="/datasource/connection/{connectionId}/alter_database",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"databaseName": {"type": "string", "description": "原数据库名称"},
|
||||
"newName": {"type": "string", "description": "新数据库名称"},
|
||||
"newOwner": {"type": "string", "description": "新所有者"},
|
||||
"newEncoding": {"type": "string", "description": "新字符编码"},
|
||||
**_API_KEY_PROP,
|
||||
},
|
||||
required=["connectionId", "databaseName"],
|
||||
param_categories={
|
||||
"connectionId": "path",
|
||||
"databaseName": "body",
|
||||
"newName": "body",
|
||||
"newOwner": "body",
|
||||
"newEncoding": "body",
|
||||
"apiKey": "header",
|
||||
},
|
||||
)
|
||||
|
||||
ALTER_TABLE = _tool(
|
||||
name="alter_table",
|
||||
description="修改表结构(添加列、删除列、重命名列、修改列类型等)",
|
||||
method="PUT",
|
||||
path="/datasource/connection/{connectionId}/alter_table",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"databaseName": {"type": "string", "description": "数据库名称"},
|
||||
"tableName": {"type": "string", "description": "表名"},
|
||||
"tableComment": {"type": "string", "description": "表注释"},
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"description": "操作列表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {"type": "string"},
|
||||
"column": {"type": "object"},
|
||||
},
|
||||
},
|
||||
},
|
||||
**_API_KEY_PROP,
|
||||
},
|
||||
required=["connectionId", "databaseName", "tableName", "operations"],
|
||||
param_categories={
|
||||
"connectionId": "path",
|
||||
"databaseName": "body",
|
||||
"tableName": "body",
|
||||
"tableComment": "body",
|
||||
"operations": "body",
|
||||
"apiKey": "header",
|
||||
},
|
||||
)
|
||||
|
||||
GENERATE_TABLE = _tool(
|
||||
name="generate_table",
|
||||
description="使用 AI 根据需求描述生成表结构(异步任务)",
|
||||
method="POST",
|
||||
path="/datasource/connection/generate_table",
|
||||
properties={
|
||||
"requirement": {"type": "string", "description": "需求描述"},
|
||||
"databaseId": {"type": "integer", "description": "关联的数据库ID"},
|
||||
},
|
||||
required=["requirement"],
|
||||
param_categories={"requirement": "body", "databaseId": "body"},
|
||||
)
|
||||
|
||||
IMPORT_DOCUMENT_PREVIEW = _tool(
|
||||
name="import_document_preview",
|
||||
description="上传 Excel/CSV 文件,AI 识别表结构并预览前 10 条数据",
|
||||
method="POST",
|
||||
path="/datasource/connection/{connectionId}/import_document/preview",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "目标环境(prod/test,默认 prod)",
|
||||
"default": "prod",
|
||||
},
|
||||
"filePath": {"type": "string", "description": "本地 Excel/CSV 文件路径"},
|
||||
},
|
||||
required=["connectionId", "filePath"],
|
||||
param_categories={
|
||||
"connectionId": "path",
|
||||
"target": "query",
|
||||
"filePath": "file",
|
||||
},
|
||||
)
|
||||
|
||||
IMPORT_DOCUMENT_CONFIRM = _tool(
|
||||
name="import_document_confirm",
|
||||
description="确认导入,创建表并插入数据",
|
||||
method="POST",
|
||||
path="/datasource/connection/{connectionId}/import_document/confirm",
|
||||
properties={
|
||||
"connectionId": {"type": "integer", "description": "连接实例ID"},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "目标环境(prod/test,默认 prod)",
|
||||
"default": "prod",
|
||||
},
|
||||
"tableStructure": {
|
||||
"type": "object",
|
||||
"description": "表结构定义",
|
||||
},
|
||||
"allData": {
|
||||
"type": "array",
|
||||
"description": "完整导入数据",
|
||||
},
|
||||
**_API_KEY_PROP,
|
||||
},
|
||||
required=["connectionId", "tableStructure", "allData"],
|
||||
param_categories={
|
||||
"connectionId": "path",
|
||||
"target": "query",
|
||||
"tableStructure": "body",
|
||||
"allData": "body",
|
||||
"apiKey": "header",
|
||||
},
|
||||
)
|
||||
|
||||
BUILTIN_TABLE_DATA = _tool(
|
||||
name="builtin_table_data",
|
||||
description="根据表ID查询表结构和数据(分页)",
|
||||
method="GET",
|
||||
path="/datasource/connection/builtin/table/{tableId}",
|
||||
properties={
|
||||
"tableId": {"type": "integer", "description": "表ID"},
|
||||
"pageNum": {"type": "integer", "description": "页码(默认1)"},
|
||||
"pageSize": {"type": "integer", "description": "每页数量(默认100)"},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "目标环境(prod/test,默认 prod)",
|
||||
"default": "prod",
|
||||
},
|
||||
},
|
||||
required=["tableId"],
|
||||
param_categories={
|
||||
"tableId": "path",
|
||||
"pageNum": "query",
|
||||
"pageSize": "query",
|
||||
"target": "query",
|
||||
},
|
||||
)
|
||||
|
||||
BUILTIN_TABLE_INSERT = _tool(
|
||||
name="builtin_table_insert",
|
||||
description="向指定表插入一条数据",
|
||||
method="POST",
|
||||
path="/datasource/connection/builtin/table/{tableId}/rows",
|
||||
properties={
|
||||
"tableId": {"type": "integer", "description": "表ID"},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "目标环境(prod/test,默认 prod)",
|
||||
"default": "prod",
|
||||
},
|
||||
"data": {"type": "object", "description": "要插入的数据"},
|
||||
},
|
||||
required=["tableId", "data"],
|
||||
param_categories={
|
||||
"tableId": "path",
|
||||
"target": "query",
|
||||
"data": "body",
|
||||
},
|
||||
)
|
||||
|
||||
BUILTIN_TABLE_UPDATE = _tool(
|
||||
name="builtin_table_update",
|
||||
description="根据主键更新表数据",
|
||||
method="PUT",
|
||||
path="/datasource/connection/builtin/table/{tableId}/rows",
|
||||
properties={
|
||||
"tableId": {"type": "integer", "description": "表ID"},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "目标环境(prod/test,默认 prod)",
|
||||
"default": "prod",
|
||||
},
|
||||
"data": {"type": "object", "description": "要更新的数据"},
|
||||
"primaryKey": {"type": "object", "description": "主键条件"},
|
||||
},
|
||||
required=["tableId", "data", "primaryKey"],
|
||||
param_categories={
|
||||
"tableId": "path",
|
||||
"target": "query",
|
||||
"data": "body",
|
||||
"primaryKey": "body",
|
||||
},
|
||||
)
|
||||
|
||||
BUILTIN_TABLE_DELETE = _tool(
|
||||
name="builtin_table_delete",
|
||||
description="根据主键批量删除表数据",
|
||||
method="DELETE",
|
||||
path="/datasource/connection/builtin/table/{tableId}/rows",
|
||||
properties={
|
||||
"tableId": {"type": "integer", "description": "表ID"},
|
||||
"target": {
|
||||
"type": "string",
|
||||
"description": "目标环境(prod/test,默认 prod)",
|
||||
"default": "prod",
|
||||
},
|
||||
"primaryKeys": {
|
||||
"type": "array",
|
||||
"description": "主键条件列表",
|
||||
"items": {"type": "object"},
|
||||
},
|
||||
},
|
||||
required=["tableId", "primaryKeys"],
|
||||
param_categories={
|
||||
"tableId": "path",
|
||||
"target": "query",
|
||||
"primaryKeys": "body",
|
||||
},
|
||||
)
|
||||
|
||||
ALL_TOOLS = [
|
||||
LIST_DATASOURCE_CONFIGS,
|
||||
GET_DATASOURCE_CONFIG,
|
||||
BATCH_CREATE_DATASOURCE_CONFIGS,
|
||||
REPLACE_DATASOURCE_CONFIGS,
|
||||
BATCH_UPDATE_DATASOURCE_CONFIGS,
|
||||
DELETE_DATASOURCE_CONFIGS,
|
||||
TEST_CONNECTION_CONFIG,
|
||||
CHANGE_DATASOURCE_STATUS,
|
||||
EXPORT_DATASOURCE_CONFIGS,
|
||||
LIST_CONNECTIONS,
|
||||
GET_CONNECTION,
|
||||
CREATE_CONNECTION,
|
||||
UPDATE_CONNECTION,
|
||||
DELETE_CONNECTION,
|
||||
TEST_CONNECTION,
|
||||
CHANGE_CONNECTION_STATUS,
|
||||
REALTIME_STRUCTURE,
|
||||
REALTIME_DATABASES,
|
||||
REALTIME_TABLES,
|
||||
CREATE_BUILTIN_POSTGRESQL,
|
||||
UPDATE_BUILTIN_DATABASE,
|
||||
EXECUTE_SQL,
|
||||
CREATE_DATABASE,
|
||||
CREATE_TABLE,
|
||||
CREATE_DATABASE_TABLE,
|
||||
ALTER_DATABASE,
|
||||
ALTER_TABLE,
|
||||
GENERATE_TABLE,
|
||||
IMPORT_DOCUMENT_PREVIEW,
|
||||
IMPORT_DOCUMENT_CONFIRM,
|
||||
BUILTIN_TABLE_DATA,
|
||||
BUILTIN_TABLE_INSERT,
|
||||
BUILTIN_TABLE_UPDATE,
|
||||
BUILTIN_TABLE_DELETE,
|
||||
]
|
||||
|
||||
|
||||
TOOL_MAP = {tool["name"]: tool for tool in ALL_TOOLS}
|
||||
|
||||
|
||||
def list_tools() -> List[Dict[str, Any]]:
|
||||
"""Return all tool definitions."""
|
||||
return ALL_TOOLS
|
||||
|
||||
|
||||
def get_tool(name: str) -> Dict[str, Any]:
|
||||
"""Return a tool definition by name."""
|
||||
return TOOL_MAP.get(name)
|
||||
@@ -0,0 +1,14 @@
|
||||
"""Utils package for lzwcai_mcp_agile_db_third."""
|
||||
|
||||
from .env_config import get_backend_base_url, get_api_key, get_env_config
|
||||
from .api_client import DataSourceAPIClient, api_request
|
||||
from .logger_config import logger_config
|
||||
|
||||
__all__ = [
|
||||
"get_backend_base_url",
|
||||
"get_api_key",
|
||||
"get_env_config",
|
||||
"DataSourceAPIClient",
|
||||
"api_request",
|
||||
"logger_config",
|
||||
]
|
||||
@@ -0,0 +1,223 @@
|
||||
"""Backend API client for datasource APIs."""
|
||||
|
||||
import base64
|
||||
import logging
|
||||
import mimetypes
|
||||
import os
|
||||
from typing import Any, Dict, Optional
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import httpx
|
||||
|
||||
try:
|
||||
from .env_config import get_backend_base_url
|
||||
except ImportError:
|
||||
from env_config import get_backend_base_url
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_TOKEN = (
|
||||
""
|
||||
)
|
||||
|
||||
_SENSITIVE_FIELDS = {"password", "apiKey", "token", "secret"}
|
||||
|
||||
|
||||
def _mask_secret(value: Optional[str]) -> str:
|
||||
"""Mask a secret for logging, keeping only a few leading/trailing chars."""
|
||||
if not value:
|
||||
return "<空>"
|
||||
if len(value) <= 8:
|
||||
return "***"
|
||||
return f"{value[:4]}...{value[-4:]} (len={len(value)})"
|
||||
|
||||
|
||||
class DataSourceAPIClient:
|
||||
"""HTTP client for backend datasource APIs."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
base_url: Optional[str] = None,
|
||||
token: Optional[str] = None,
|
||||
):
|
||||
self.base_url = (base_url or get_backend_base_url()).rstrip("/")
|
||||
# Prefer explicit token, then API_KEY env var, then built-in default.
|
||||
resolved = token or os.environ.get("API_KEY") or DEFAULT_TOKEN
|
||||
self.token = resolved.removeprefix("Bearer ").strip()
|
||||
self.client = httpx.Client(timeout=120.0)
|
||||
|
||||
def close(self) -> None:
|
||||
"""Close the underlying HTTP client."""
|
||||
self.client.close()
|
||||
|
||||
def _get_headers(self, api_key: Optional[str] = None) -> Dict[str, str]:
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.token}",
|
||||
"Accept": "application/json",
|
||||
}
|
||||
if api_key:
|
||||
headers["X-Datasource-API-Key"] = api_key
|
||||
return headers
|
||||
|
||||
@staticmethod
|
||||
def _sanitize_value(value: Any) -> Any:
|
||||
"""Redact sensitive nested values for logging."""
|
||||
if isinstance(value, dict):
|
||||
return {
|
||||
k: "***" if k in _SENSITIVE_FIELDS else DataSourceAPIClient._sanitize_value(v)
|
||||
for k, v in value.items()
|
||||
}
|
||||
if isinstance(value, list):
|
||||
return [DataSourceAPIClient._sanitize_value(item) for item in value]
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _file_tuple(file_path: str) -> tuple:
|
||||
"""Build a multipart file tuple with filename and content type."""
|
||||
filename = os.path.basename(file_path)
|
||||
content_type, _ = mimetypes.guess_type(filename)
|
||||
if content_type is None:
|
||||
content_type = "application/octet-stream"
|
||||
return (filename, open(file_path, "rb"), content_type)
|
||||
|
||||
def request(
|
||||
self,
|
||||
method: str,
|
||||
path_template: str,
|
||||
path_params: Optional[Dict[str, Any]] = None,
|
||||
query_params: Optional[Dict[str, Any]] = None,
|
||||
body: Optional[Dict[str, Any]] = None,
|
||||
file_path: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Send a request and return JSON response."""
|
||||
path_params = path_params or {}
|
||||
query_params = query_params or {}
|
||||
|
||||
try:
|
||||
url = self._build_url(path_template, path_params, query_params)
|
||||
headers = self._get_headers(api_key)
|
||||
|
||||
safe_body = self._sanitize_value(body) if body else body
|
||||
logger.info(f"调用后端 API: {method} {url}")
|
||||
logger.info(
|
||||
f"请求密钥 - X-Datasource-API-Key: {_mask_secret(api_key)}, "
|
||||
f"Authorization token: {_mask_secret(self.token)}"
|
||||
)
|
||||
logger.debug(
|
||||
f"path_params={path_params}, query_params={query_params}, "
|
||||
f"body={safe_body}, file_path={file_path}"
|
||||
)
|
||||
|
||||
method = method.upper()
|
||||
if method == "GET":
|
||||
response = self.client.get(url, headers=headers)
|
||||
elif method == "DELETE":
|
||||
if body is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
response = self.client.request(method, url, headers=headers, json=body)
|
||||
else:
|
||||
response = self.client.delete(url, headers=headers)
|
||||
elif file_path:
|
||||
# Multipart upload
|
||||
headers.pop("Content-Type", None)
|
||||
file_tuple = self._file_tuple(file_path)
|
||||
files = {"file": file_tuple}
|
||||
try:
|
||||
response = self.client.request(
|
||||
method, url, headers=headers, data=body, files=files
|
||||
)
|
||||
finally:
|
||||
file_tuple[1].close()
|
||||
elif body is not None:
|
||||
headers["Content-Type"] = "application/json"
|
||||
response = self.client.request(method, url, headers=headers, json=body)
|
||||
else:
|
||||
response = self.client.request(method, url, headers=headers)
|
||||
|
||||
response.raise_for_status()
|
||||
content_type = response.headers.get("content-type", "")
|
||||
if "application/json" in content_type:
|
||||
data = response.json()
|
||||
else:
|
||||
# Non-JSON response (e.g., Excel export), encode as base64
|
||||
data = {
|
||||
"success": True,
|
||||
"contentType": content_type,
|
||||
"fileBase64": base64.b64encode(response.content).decode("ascii"),
|
||||
"message": "后端返回非 JSON 内容,已使用 base64 编码",
|
||||
}
|
||||
logger.info(f"后端 API 调用成功: {method} {url}")
|
||||
logger.debug(f"响应: {data}")
|
||||
return data
|
||||
|
||||
except httpx.TimeoutException as e:
|
||||
error_msg = f"API 请求超时: {method} {path_template}"
|
||||
logger.error(error_msg)
|
||||
raise Exception(error_msg) from e
|
||||
except httpx.HTTPStatusError as e:
|
||||
error_msg = f"API 请求失败 (HTTP {e.response.status_code}): {e.response.text}"
|
||||
logger.error(error_msg)
|
||||
raise Exception(error_msg) from e
|
||||
except httpx.RequestError as e:
|
||||
error_msg = f"API 请求异常: {method} {path_template}, 错误: {e}"
|
||||
logger.error(error_msg)
|
||||
raise Exception(error_msg) from e
|
||||
except Exception as e:
|
||||
error_msg = f"处理 API 响应时出错: {e}"
|
||||
logger.error(error_msg, exc_info=True)
|
||||
raise Exception(error_msg) from e
|
||||
|
||||
def _build_url(
|
||||
self,
|
||||
path_template: str,
|
||||
path_params: Dict[str, Any],
|
||||
query_params: Dict[str, Any],
|
||||
) -> str:
|
||||
path = path_template
|
||||
for key, value in path_params.items():
|
||||
path = path.replace(f"{{{key}}}", str(value))
|
||||
|
||||
url = f"{self.base_url}{path}"
|
||||
|
||||
if query_params:
|
||||
filtered = {k: v for k, v in query_params.items() if v is not None}
|
||||
if filtered:
|
||||
url = f"{url}?{urlencode(filtered)}"
|
||||
|
||||
return url
|
||||
|
||||
|
||||
def api_request(
|
||||
method: str,
|
||||
path_template: str,
|
||||
path_params: Optional[Dict[str, Any]] = None,
|
||||
query_params: Optional[Dict[str, Any]] = None,
|
||||
body: Optional[Dict[str, Any]] = None,
|
||||
file_path: Optional[str] = None,
|
||||
api_key: Optional[str] = None,
|
||||
base_url: Optional[str] = None,
|
||||
token: Optional[str] = None,
|
||||
) -> Dict[str, Any]:
|
||||
"""Convenience wrapper for one-off API requests."""
|
||||
client = DataSourceAPIClient(base_url=base_url, token=token)
|
||||
try:
|
||||
return client.request(
|
||||
method=method,
|
||||
path_template=path_template,
|
||||
path_params=path_params,
|
||||
query_params=query_params,
|
||||
body=body,
|
||||
file_path=file_path,
|
||||
api_key=api_key,
|
||||
)
|
||||
finally:
|
||||
client.close()
|
||||
|
||||
|
||||
# Module-level default client
|
||||
_default_client = DataSourceAPIClient()
|
||||
|
||||
|
||||
def get_default_client() -> DataSourceAPIClient:
|
||||
return _default_client
|
||||
@@ -0,0 +1,31 @@
|
||||
"""环境变量配置模块"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def get_backend_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str:
|
||||
"""
|
||||
获取后端 API 基础 URL。
|
||||
|
||||
Environment Variables:
|
||||
backendBaseUrl: 后端 API 基础 URL
|
||||
"""
|
||||
return os.environ.get("backendBaseUrl", default)
|
||||
|
||||
|
||||
def get_api_key(default: str = "") -> str:
|
||||
"""
|
||||
获取默认 API 密钥(X-Datasource-API-Key)。
|
||||
|
||||
Environment Variables:
|
||||
datasourceApiKey: 默认 API 密钥
|
||||
"""
|
||||
return os.environ.get("datasourceApiKey", default)
|
||||
|
||||
|
||||
def get_env_config() -> dict:
|
||||
"""获取所有环境配置。"""
|
||||
return {
|
||||
"backend_base_url": get_backend_base_url(),
|
||||
"api_key": get_api_key(),
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
"""统一日志配置模块"""
|
||||
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class LoggerConfig:
|
||||
"""日志配置管理类"""
|
||||
|
||||
def __init__(self, logs_dir: str = None):
|
||||
if logs_dir:
|
||||
self.logs_dir = Path(logs_dir)
|
||||
else:
|
||||
project_root = Path(__file__).parent.parent
|
||||
self.logs_dir = project_root / "logs"
|
||||
self.logs_dir.mkdir(parents=True, exist_ok=True)
|
||||
self.log_format = "%(asctime)s - %(name)s - %(levelname)s - [%(filename)s:%(lineno)d] - %(message)s"
|
||||
self.date_format = "%Y-%m-%d %H:%M:%S"
|
||||
self._initialized = False
|
||||
|
||||
def setup_logging(
|
||||
self,
|
||||
app_name: str = "lzwcai_mcp_agile_db_third",
|
||||
log_level: int = logging.INFO,
|
||||
console_output: bool = False,
|
||||
) -> logging.Logger:
|
||||
"""设置系统日志配置。"""
|
||||
if self._initialized:
|
||||
return logging.getLogger()
|
||||
|
||||
root_logger = logging.getLogger()
|
||||
root_logger.setLevel(log_level)
|
||||
|
||||
for handler in root_logger.handlers[:]:
|
||||
root_logger.removeHandler(handler)
|
||||
|
||||
formatter = logging.Formatter(self.log_format, self.date_format)
|
||||
|
||||
# 主日志文件
|
||||
main_log_file = self.logs_dir / f"{app_name}.log"
|
||||
file_handler = logging.FileHandler(main_log_file, encoding="utf-8")
|
||||
file_handler.setLevel(log_level)
|
||||
file_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(file_handler)
|
||||
|
||||
# 错误日志文件
|
||||
error_log_file = self.logs_dir / f"{app_name}_error.log"
|
||||
error_handler = logging.FileHandler(error_log_file, encoding="utf-8")
|
||||
error_handler.setLevel(logging.ERROR)
|
||||
error_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(error_handler)
|
||||
|
||||
# MCP 使用 stdio 时,控制台日志必须输出到 stderr
|
||||
if console_output:
|
||||
console_handler = logging.StreamHandler(sys.stderr)
|
||||
console_handler.setLevel(log_level)
|
||||
console_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(console_handler)
|
||||
|
||||
self._initialized = True
|
||||
root_logger.info(f"日志系统初始化完成 - 日志目录: {self.logs_dir}")
|
||||
return root_logger
|
||||
|
||||
def setup_mcp_logging(self) -> logging.Logger:
|
||||
"""设置 MCP 专用日志。"""
|
||||
return self.create_component_logger("mcp_services", "mcp_services.log", logging.DEBUG)
|
||||
|
||||
def create_component_logger(
|
||||
self, component_name: str, log_file: str = None, log_level: int = None
|
||||
) -> logging.Logger:
|
||||
"""为特定组件创建独立日志器。"""
|
||||
logger = logging.getLogger(component_name)
|
||||
if log_file:
|
||||
component_log_file = self.logs_dir / log_file
|
||||
handler = logging.FileHandler(component_log_file, encoding="utf-8")
|
||||
handler.setFormatter(logging.Formatter(self.log_format, self.date_format))
|
||||
if log_level is not None:
|
||||
handler.setLevel(log_level)
|
||||
logger.addHandler(handler)
|
||||
return logger
|
||||
|
||||
|
||||
logger_config = LoggerConfig()
|
||||
17
lzwcai_mcp_agile_db_third/main.py
Normal file
17
lzwcai_mcp_agile_db_third/main.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""
|
||||
Repository-local launcher for lzwcai-mcp-sqlexecutor.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
|
||||
def main():
|
||||
# Keep local developer defaults without overriding explicit environment settings.
|
||||
os.environ.setdefault("backendBaseUrl", "http://192.168.2.236:8088")
|
||||
os.environ.setdefault("datasourceApiKey", "yBOHyhCSpExAoEleimSfhbRzsF6SDiPYdGGwowXG-Sk")
|
||||
from lzwcai_mcp_agile_db_third.main import main
|
||||
main()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
41
lzwcai_mcp_agile_db_third/pyproject.toml
Normal file
41
lzwcai_mcp_agile_db_third/pyproject.toml
Normal file
@@ -0,0 +1,41 @@
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "lzwcai-mcp-agile-db-third"
|
||||
version = "0.1.6"
|
||||
description = "MCP server for Agile DB third-party datasource APIs"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
license = {text = "MIT"}
|
||||
authors = [
|
||||
{name = "lzwcai", email = "your-email@example.com"},
|
||||
]
|
||||
keywords = ["mcp", "datasource", "database", "agile-db"]
|
||||
classifiers = [
|
||||
"Development Status :: 3 - Alpha",
|
||||
"Intended Audience :: Developers",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
]
|
||||
dependencies = [
|
||||
"httpx>=0.28.1",
|
||||
"mcp[cli]>=1.10.1",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
lzwcai-mcp-agile-db-third = "lzwcai_mcp_agile_db_third.main:main"
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["lzwcai_mcp_agile_db_third"]
|
||||
|
||||
[tool.hatch.build.targets.wheel.force-include]
|
||||
|
||||
[tool.hatch.build]
|
||||
exclude = [
|
||||
"lzwcai_mcp_agile_db_third/logs/**",
|
||||
"**/.__pycache__/**",
|
||||
"lzwcai_mcp_agile_db_third/.gitignore",
|
||||
".venv/**",
|
||||
]
|
||||
418
lzwcai_mcp_agile_db_third/selftest_tools.py
Normal file
418
lzwcai_mcp_agile_db_third/selftest_tools.py
Normal file
@@ -0,0 +1,418 @@
|
||||
"""End-to-end self-test for all lzwcai_mcp_agile_db_third MCP tools.
|
||||
|
||||
Runs every tool through the real MCP handler (handle_call_tool) against the
|
||||
backend configured below. Uses selftest_-prefixed throwaway resources and
|
||||
cleans them up at the end. The destructive full-replace tool is skipped.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
|
||||
os.environ.setdefault("backendBaseUrl", "http://192.168.2.236:8088")
|
||||
# Login token (Authorization Bearer) shared with the first-party AgileDB MCP server.
|
||||
os.environ.setdefault(
|
||||
"API_KEY",
|
||||
"Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6"
|
||||
"ImNlMDAwYjA4LWU0YTYtNGM2MS1hNzJiLWI3NTlmNmY1N2Q4NCJ9.jiNmGQZfL4-nSIFrLuaCt7mT"
|
||||
"5zj0FOojAVkLeHwPOroI5jBxodrCe1PSwGO1OHq5Ztb0tLEVZw2FFVj0OlTceQ",
|
||||
)
|
||||
# Optional X-Datasource-API-Key for datasource-level permission checks (if enforced).
|
||||
os.environ.setdefault("datasourceApiKey", "Mggkz34Yk8cbjUvCvQ-qeooNRg62WhSwwtxUUV6e0Pg")
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
sys.path.insert(0, HERE)
|
||||
|
||||
from lzwcai_mcp_agile_db_third.main import handle_call_tool # noqa: E402
|
||||
from lzwcai_mcp_agile_db_third.mock_db import start as start_mock_db, stop as stop_mock_db # noqa: E402
|
||||
from lzwcai_mcp_agile_db_third.tools import ALL_TOOLS # noqa: E402
|
||||
|
||||
RESULTS = [] # (name, status, detail)
|
||||
TESTED = set()
|
||||
|
||||
|
||||
async def call(name, args=None):
|
||||
"""Invoke a tool through the MCP handler and return parsed JSON."""
|
||||
TESTED.add(name)
|
||||
try:
|
||||
resp = await handle_call_tool(name, args or {})
|
||||
return json.loads(resp[0].text)
|
||||
except Exception as e: # noqa: BLE001
|
||||
return {"_exception": repr(e)}
|
||||
|
||||
|
||||
def summarize(data):
|
||||
"""Extract a short (ok, detail) from a backend response."""
|
||||
if not isinstance(data, dict):
|
||||
return False, str(data)[:200]
|
||||
if "_exception" in data:
|
||||
return False, data["_exception"][:200]
|
||||
if "error" in data:
|
||||
return False, str(data["error"])[:200]
|
||||
code = data.get("code")
|
||||
msg = data.get("msg", "")
|
||||
if code == 200:
|
||||
return True, f"code=200 msg={msg}"
|
||||
if code is not None:
|
||||
return False, f"code={code} msg={msg}"
|
||||
# list/paginated or already-unwrapped payloads
|
||||
return True, json.dumps(data, ensure_ascii=False)[:160]
|
||||
|
||||
|
||||
def record(name, data, mode="ok"):
|
||||
"""Record a tool result.
|
||||
|
||||
mode="ok" -> PASS only when backend code==200
|
||||
mode="roundtrip" -> PASS when the call round-trips (no exception and the
|
||||
backend returned a structured response), regardless of
|
||||
business code. Used for ops whose success depends on a
|
||||
real reachable DB we can't guarantee in self-test.
|
||||
"""
|
||||
ok, detail = summarize(data)
|
||||
if mode == "roundtrip":
|
||||
exception = isinstance(data, dict) and "_exception" in data
|
||||
passed = not exception
|
||||
else:
|
||||
passed = ok
|
||||
status = "PASS" if passed else "FAIL"
|
||||
RESULTS.append((name, status, detail))
|
||||
print(f"[{status}] {name}: {detail}")
|
||||
return data
|
||||
|
||||
|
||||
def dig(data, *keys, default=None):
|
||||
"""Safely walk nested dict keys."""
|
||||
cur = data
|
||||
for k in keys:
|
||||
if not isinstance(cur, dict):
|
||||
return default
|
||||
cur = cur.get(k)
|
||||
return cur if cur is not None else default
|
||||
|
||||
|
||||
async def main():
|
||||
PREFIX = f"selftest_{uuid.uuid4().hex[:8]}_"
|
||||
state = {}
|
||||
|
||||
# ---- 1. read-only: connections & configs ----------------------------
|
||||
print("\n=== 只读查询 ===")
|
||||
conns = record("list_connections", await call("list_connections", {"pageSize": 5}))
|
||||
# pick a builtin connection to exercise builtin/realtime tools
|
||||
rows = conns.get("rows") or dig(conns, "data", "rows", default=[]) or []
|
||||
builtin_id = None
|
||||
any_conn_id = None
|
||||
for r in rows:
|
||||
cid = r.get("id")
|
||||
if any_conn_id is None:
|
||||
any_conn_id = cid
|
||||
if r.get("sourceType") == "builtin" and builtin_id is None:
|
||||
builtin_id = cid
|
||||
state["builtin_id"] = builtin_id
|
||||
state["any_conn_id"] = any_conn_id
|
||||
print(f" -> builtin_id={builtin_id}, any_conn_id={any_conn_id}")
|
||||
|
||||
record("list_datasource_configs", await call("list_datasource_configs", {"pageSize": 5}))
|
||||
|
||||
if any_conn_id:
|
||||
record("get_connection", await call("get_connection", {"id": any_conn_id}))
|
||||
record("realtime_databases", await call("realtime_databases", {"id": any_conn_id}))
|
||||
record("realtime_structure", await call("realtime_structure", {"id": any_conn_id}))
|
||||
else:
|
||||
for n in ("get_connection", "realtime_databases", "realtime_structure"):
|
||||
RESULTS.append((n, "SKIP", "no connection available"))
|
||||
|
||||
# ---- 2. create a builtin PostgreSQL connection ----------------------
|
||||
print("\n=== 创建内置连接 ===")
|
||||
created = record(
|
||||
"create_builtin_postgresql",
|
||||
await call("create_builtin_postgresql", {
|
||||
"datasourceName": PREFIX + "conn",
|
||||
"remark": "self-test connection",
|
||||
}),
|
||||
)
|
||||
conn_id = dig(created, "data", "id")
|
||||
if conn_id is None and builtin_id:
|
||||
conn_id = builtin_id # fall back to an existing builtin for downstream tools
|
||||
state["conn_id"] = conn_id
|
||||
print(f" -> conn_id={conn_id}")
|
||||
|
||||
if conn_id:
|
||||
record("update_builtin_database", await call("update_builtin_database", {
|
||||
"connectionId": conn_id,
|
||||
"datasourceName": PREFIX + "conn_renamed",
|
||||
"remark": "renamed by self-test",
|
||||
}))
|
||||
record("get_connection", await call("get_connection", {"id": conn_id}))
|
||||
|
||||
# ---- 3. external connection: test/create/update/status/delete -------
|
||||
print("\n=== 外部连接测试(依赖真实可达的库,可能失败属正常)===")
|
||||
# Try to spin up a local Docker Postgres mock. If Docker is unavailable or
|
||||
# the backend cannot reach the host IP, fall back to 127.0.0.1 and keep the
|
||||
# round-trip assertion (we still verify the tool round-trips).
|
||||
mock_host = "127.0.0.1"
|
||||
try:
|
||||
mock_host = start_mock_db()
|
||||
print(f" -> mock DB available at {mock_host}:5432")
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" -> could not start mock DB, using 127.0.0.1:5432 ({e})")
|
||||
|
||||
# test_connection_config / test_connection rely on datasourceType default now
|
||||
record(
|
||||
"test_connection_config",
|
||||
await call("test_connection_config", {
|
||||
"host": mock_host, "port": 5432,
|
||||
"username": "postgres", "password": "postgres",
|
||||
}),
|
||||
mode="roundtrip", # may still fail if backend cannot reach mock_host
|
||||
)
|
||||
|
||||
# create_connection (external) — exercises datasourceType/connectionType defaults
|
||||
ext = record(
|
||||
"create_connection",
|
||||
await call("create_connection", {
|
||||
"datasourceName": PREFIX + "ext",
|
||||
"host": mock_host, "port": 5432,
|
||||
"username": "postgres", "password": "postgres",
|
||||
"remark": "self-test external",
|
||||
}),
|
||||
mode="roundtrip",
|
||||
)
|
||||
ext_id = dig(ext, "data", "id")
|
||||
state["ext_id"] = ext_id
|
||||
print(f" -> ext_id={ext_id}")
|
||||
|
||||
if ext_id:
|
||||
record("update_connection", await call("update_connection", {
|
||||
"id": ext_id, "datasourceName": PREFIX + "ext_renamed",
|
||||
"remark": "renamed",
|
||||
}), mode="roundtrip")
|
||||
record("test_connection", await call("test_connection", {
|
||||
"id": ext_id, "host": mock_host, "port": 5432,
|
||||
"username": "postgres", "password": "postgres",
|
||||
}), mode="roundtrip")
|
||||
record("change_connection_status", await call("change_connection_status", {
|
||||
"id": ext_id, "status": 1,
|
||||
}), mode="roundtrip")
|
||||
else:
|
||||
for n in ("update_connection", "test_connection", "change_connection_status"):
|
||||
RESULTS.append((n, "SKIP", "no external connection id returned"))
|
||||
|
||||
# ---- 4. DDL on the builtin connection ------------------------------
|
||||
print("\n=== DDL:库/表 ===")
|
||||
ddl_conn = conn_id or builtin_id
|
||||
db_name = PREFIX + "db"
|
||||
tbl_name = PREFIX + "users"
|
||||
cols = [
|
||||
{"columnName": "id", "columnType": "BIGINT", "isPrimaryKey": True,
|
||||
"isNullable": False, "columnComment": "主键"},
|
||||
{"columnName": "name", "columnType": "VARCHAR", "columnLength": 100,
|
||||
"isNullable": False, "columnComment": "姓名"},
|
||||
{"columnName": "age", "columnType": "INTEGER", "isNullable": True,
|
||||
"columnComment": "年龄"},
|
||||
]
|
||||
if ddl_conn:
|
||||
record("create_database", await call("create_database", {
|
||||
"connectionId": ddl_conn, "databaseName": db_name,
|
||||
"encoding": "UTF8",
|
||||
}), mode="roundtrip")
|
||||
record("create_table", await call("create_table", {
|
||||
"connectionId": ddl_conn, "databaseName": db_name,
|
||||
"tableName": tbl_name, "tableComment": "self-test 用户表",
|
||||
"columns": cols,
|
||||
}), mode="roundtrip")
|
||||
record("realtime_tables", await call("realtime_tables", {
|
||||
"id": ddl_conn, "databaseName": db_name,
|
||||
}), mode="roundtrip")
|
||||
record("create_database_table", await call("create_database_table", {
|
||||
"connectionId": ddl_conn, "databaseName": PREFIX + "db2",
|
||||
"encoding": "UTF8",
|
||||
"tables": [{"tableName": PREFIX + "t2", "tableComment": "二号表",
|
||||
"columns": cols}],
|
||||
}), mode="roundtrip")
|
||||
record("alter_table", await call("alter_table", {
|
||||
"connectionId": ddl_conn, "databaseName": db_name,
|
||||
"tableName": tbl_name,
|
||||
"operations": [{"operation": "ADD_COLUMN", "column": {
|
||||
"columnName": "email", "columnType": "VARCHAR",
|
||||
"columnLength": 255, "isNullable": True, "columnComment": "邮箱"}}],
|
||||
}), mode="roundtrip")
|
||||
record("alter_database", await call("alter_database", {
|
||||
"connectionId": ddl_conn, "databaseName": PREFIX + "db2",
|
||||
"newName": PREFIX + "db2_renamed",
|
||||
}), mode="roundtrip")
|
||||
else:
|
||||
for n in ("create_database", "create_table", "create_database_table",
|
||||
"alter_table", "alter_database"):
|
||||
RESULTS.append((n, "SKIP", "no builtin connection for DDL"))
|
||||
|
||||
# ---- 5. find the table id, exercise execute_sql + builtin CRUD ------
|
||||
print("\n=== SQL 执行 + 内置表数据 CRUD ===")
|
||||
# locate a datasource config id + table id for our table
|
||||
cfgs = await call("list_datasource_configs", {"datasourceName": PREFIX, "pageSize": 20})
|
||||
cfg_rows = cfgs.get("rows") or dig(cfgs, "data", "rows", default=[]) or []
|
||||
datasource_id = cfg_rows[0].get("id") if cfg_rows else None
|
||||
state["datasource_id"] = datasource_id
|
||||
|
||||
table_id = None
|
||||
if ddl_conn:
|
||||
detail = await call("get_connection", {"id": ddl_conn})
|
||||
for ds in dig(detail, "data", "datasourceConfig", default=[]) or []:
|
||||
for t in ds.get("tables", []) or []:
|
||||
if str(t.get("tableName", "")).startswith(PREFIX):
|
||||
table_id = t.get("tableId") or t.get("id")
|
||||
break
|
||||
if table_id:
|
||||
break
|
||||
state["table_id"] = table_id
|
||||
print(f" -> datasource_id={datasource_id}, table_id={table_id}")
|
||||
|
||||
if datasource_id:
|
||||
record("execute_sql", await call("execute_sql", {
|
||||
"datasourceId": datasource_id,
|
||||
"sql": "SELECT 1",
|
||||
"databaseName": db_name,
|
||||
}), mode="roundtrip")
|
||||
else:
|
||||
RESULTS.append(("execute_sql", "SKIP", "no datasource config id"))
|
||||
|
||||
if table_id:
|
||||
record("builtin_table_insert", await call("builtin_table_insert", {
|
||||
"tableId": table_id, "data": {"id": 1, "name": "张三", "age": 25},
|
||||
}), mode="roundtrip")
|
||||
record("builtin_table_data", await call("builtin_table_data", {
|
||||
"tableId": table_id,
|
||||
}), mode="roundtrip")
|
||||
record("builtin_table_update", await call("builtin_table_update", {
|
||||
"tableId": table_id, "data": {"name": "李四", "age": 30},
|
||||
"primaryKey": {"id": 1},
|
||||
}), mode="roundtrip")
|
||||
record("builtin_table_delete", await call("builtin_table_delete", {
|
||||
"tableId": table_id, "primaryKeys": [{"id": 1}],
|
||||
}), mode="roundtrip")
|
||||
else:
|
||||
for n in ("builtin_table_insert", "builtin_table_data",
|
||||
"builtin_table_update", "builtin_table_delete"):
|
||||
RESULTS.append((n, "SKIP", "no table id found"))
|
||||
|
||||
# ---- 6. AI generate + document import (preview/confirm) -------------
|
||||
print("\n=== AI 生成 + 文档导入 ===")
|
||||
record("generate_table", await call("generate_table", {
|
||||
"requirement": "一个简单的待办事项表,含标题、状态、创建时间",
|
||||
"databaseId": datasource_id,
|
||||
}), mode="roundtrip")
|
||||
|
||||
# build a tiny CSV for import preview
|
||||
csv_path = os.path.join(tempfile.gettempdir(), "selftest_import.csv")
|
||||
with open(csv_path, "w", newline="", encoding="utf-8") as f:
|
||||
w = csv.writer(f)
|
||||
w.writerow(["name", "age", "gender"])
|
||||
w.writerow(["张三", "25", "male"])
|
||||
w.writerow(["李四", "30", "female"])
|
||||
|
||||
preview = None
|
||||
if ddl_conn:
|
||||
preview = record("import_document_preview", await call("import_document_preview", {
|
||||
"connectionId": ddl_conn, "filePath": csv_path,
|
||||
}), mode="roundtrip")
|
||||
ts = dig(preview, "data", "tableStructure")
|
||||
all_data = dig(preview, "data", "allData")
|
||||
if ts and all_data:
|
||||
record("import_document_confirm", await call("import_document_confirm", {
|
||||
"connectionId": ddl_conn, "tableStructure": ts, "allData": all_data,
|
||||
}), mode="roundtrip")
|
||||
else:
|
||||
RESULTS.append(("import_document_confirm", "SKIP",
|
||||
"preview returned no tableStructure/allData"))
|
||||
else:
|
||||
for n in ("import_document_preview", "import_document_confirm"):
|
||||
RESULTS.append((n, "SKIP", "no builtin connection"))
|
||||
|
||||
# ---- 7. datasource config batch ops --------------------------------
|
||||
print("\n=== 数据源配置批量操作 ===")
|
||||
if ddl_conn:
|
||||
record("batch_create_datasource_configs", await call(
|
||||
"batch_create_datasource_configs", {
|
||||
"connectionId": ddl_conn,
|
||||
"datasourceNamePrefix": PREFIX + "cfg",
|
||||
"syncTables": False,
|
||||
"databases": [{"databaseName": db_name, "tableNames": []}],
|
||||
}), mode="roundtrip")
|
||||
else:
|
||||
RESULTS.append(("batch_create_datasource_configs", "SKIP", "no connection"))
|
||||
|
||||
if datasource_id:
|
||||
record("get_datasource_config", await call("get_datasource_config", {
|
||||
"id": datasource_id}), mode="roundtrip")
|
||||
record("change_datasource_status", await call("change_datasource_status", {
|
||||
"id": datasource_id, "status": 1}), mode="roundtrip")
|
||||
record("batch_update_datasource_configs", await call(
|
||||
"batch_update_datasource_configs", {
|
||||
"syncTables": False,
|
||||
"datasources": [{"id": datasource_id, "tableNames": []}],
|
||||
}), mode="roundtrip")
|
||||
else:
|
||||
for n in ("get_datasource_config", "change_datasource_status",
|
||||
"batch_update_datasource_configs"):
|
||||
RESULTS.append((n, "SKIP", "no datasource config id"))
|
||||
|
||||
record("export_datasource_configs", await call("export_datasource_configs", {
|
||||
"datasourceName": PREFIX}), mode="roundtrip")
|
||||
|
||||
# replace_datasource_configs intentionally skipped (destructive)
|
||||
RESULTS.append(("replace_datasource_configs", "SKIP",
|
||||
"destructive full-replace, skipped by design"))
|
||||
|
||||
# ---- 8. cleanup -----------------------------------------------------
|
||||
print("\n=== 清理 selftest_ 资源 ===")
|
||||
# delete selftest datasource configs
|
||||
cfgs2 = await call("list_datasource_configs", {"datasourceName": PREFIX, "pageSize": 50})
|
||||
cfg_rows2 = cfgs2.get("rows") or dig(cfgs2, "data", "rows", default=[]) or []
|
||||
del_ids = [r.get("id") for r in cfg_rows2 if r.get("id") is not None]
|
||||
if del_ids:
|
||||
record("delete_datasource_configs", await call("delete_datasource_configs", {
|
||||
"ids": del_ids}), mode="roundtrip")
|
||||
else:
|
||||
RESULTS.append(("delete_datasource_configs", "SKIP", "nothing to delete"))
|
||||
|
||||
# delete selftest connections
|
||||
deleted_conn = False
|
||||
for cid in (state.get("ext_id"), conn_id):
|
||||
if cid:
|
||||
record("delete_connection", await call("delete_connection", {"id": cid}),
|
||||
mode="roundtrip")
|
||||
deleted_conn = True
|
||||
if not deleted_conn:
|
||||
RESULTS.append(("delete_connection", "SKIP", "no selftest connection to delete"))
|
||||
|
||||
try:
|
||||
os.remove(csv_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
try:
|
||||
stop_mock_db()
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f" -> failed to stop mock DB: {e}")
|
||||
|
||||
# ---- summary --------------------------------------------------------
|
||||
print("\n" + "=" * 60)
|
||||
print("自测结果汇总")
|
||||
print("=" * 60)
|
||||
all_names = {t["name"] for t in ALL_TOOLS}
|
||||
counts = {"PASS": 0, "FAIL": 0, "SKIP": 0}
|
||||
for name, status, detail in RESULTS:
|
||||
counts[status] = counts.get(status, 0) + 1
|
||||
for name, status, detail in RESULTS:
|
||||
print(f" [{status}] {name}: {detail}")
|
||||
untested = all_names - TESTED
|
||||
print("-" * 60)
|
||||
print(f"工具总数: {len(all_names)} 覆盖: {len(TESTED)} 未触达: {sorted(untested)}")
|
||||
print(f"PASS={counts['PASS']} FAIL={counts['FAIL']} SKIP={counts['SKIP']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
4
lzwcai_mcp_agile_db_third/uv.toml
Normal file
4
lzwcai_mcp_agile_db_third/uv.toml
Normal file
@@ -0,0 +1,4 @@
|
||||
[[index]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple/"
|
||||
default = true
|
||||
@@ -94,3 +94,11 @@
|
||||
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:220] - 文件输出: True
|
||||
2026-03-18 18:16:52 - lzwcai_mcp_api_converter.src.util.logger_config - INFO - [logger_config.py:221] - 文件轮转: 最大10MB, 保留5个备份
|
||||
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/
|
||||
188
lzwcai_mcpskills_generate_reports/README.md
Normal file
188
lzwcai_mcpskills_generate_reports/README.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# lzwcai-mcpskills-generate-reports
|
||||
|
||||
用户提供 **docx 模板 + JSON 数据**,本包负责渲染成 docx,并可选做样式迁移。
|
||||
|
||||
本包**不内置模板**,模板完全由调用方维护。
|
||||
|
||||
## 安装
|
||||
|
||||
```powershell
|
||||
cd lzwcai_mcpskills_generate_reports
|
||||
pip install -e .
|
||||
```
|
||||
|
||||
## Python API
|
||||
|
||||
### 核心入口 `generate` / `scan_template`
|
||||
|
||||
```python
|
||||
from lzwcai_mcpskills_generate_reports import generate, scan_template
|
||||
|
||||
# 扫描模板需要哪些占位符 / for / if 块
|
||||
result = scan_template("./模板.docx") # 本地路径或 http/https URL
|
||||
|
||||
# 渲染
|
||||
out_path = generate(
|
||||
data="data.json", # dict 或 JSON 文件路径
|
||||
template="./模板.docx", # 本地路径或 http/https URL(自动下载)
|
||||
out_path="_out/报价方案.docx",
|
||||
style_ref="./用户样式.docx", # 可选:把用户模板的 theme/字体套过来
|
||||
)
|
||||
```
|
||||
|
||||
### 便捷封装 `main` 模块
|
||||
|
||||
`main` 模块在核心入口之上做了两点增强,适合程序化调用:
|
||||
|
||||
1. `data` 除 dict / 本地 JSON 路径外,还支持 **JSON 文件 URL**(自动下载、用完即删)。
|
||||
2. `out` **可省略**;省略时落到当前目录 `_out/`,文件名按 `模板名_时间戳.docx` 自动生成。
|
||||
|
||||
```python
|
||||
from lzwcai_mcpskills_generate_reports.main import generate_report, scan_report
|
||||
|
||||
# data 传 dict,out 省略 -> 默认 _out/ 下自动命名
|
||||
generate_report(template="./模板.docx", data={...})
|
||||
|
||||
# data 传本地 JSON 路径
|
||||
generate_report(template="./模板.docx", data="data.json", out="_out/a.docx")
|
||||
|
||||
# template / data 都传 URL
|
||||
generate_report(
|
||||
template="https://host/模板.docx",
|
||||
data="https://host/data.json",
|
||||
)
|
||||
|
||||
# 扫描占位符,支持本地路径或 URL
|
||||
scan_report(template="https://host/模板.docx")
|
||||
```
|
||||
|
||||
`generate_report` 返回 `{"output": 输出文件绝对路径}`。
|
||||
|
||||
### 扫描结果结构
|
||||
|
||||
```python
|
||||
# scan_template / scan_report 返回:
|
||||
# {
|
||||
# "placeholders": ["project_title", "contact_person", "equipments", ...],
|
||||
# "blocks": [
|
||||
# {"type": "for", "iterator": "eq", "variable": "equipments"},
|
||||
# {"type": "if", "condition": "show_layout"},
|
||||
# ...
|
||||
# ]
|
||||
# }
|
||||
```
|
||||
|
||||
## MCP Server
|
||||
|
||||
本包同时提供 MCP Server(stdio 模式),把渲染引擎暴露成 2 个 MCP 工具:
|
||||
|
||||
| 工具 | 说明 | 参数 |
|
||||
|------|------|------|
|
||||
| `generate_report` | 模板 + 数据 → 渲染输出 docx,返回输出文件绝对路径 | 必填 `template`、`data`;可选 `out`(省略落到 `_out/` 自动命名)、`style_ref` |
|
||||
| `scan_template` | 扫描模板占位符与 for/if 块结构 | 必填 `template` |
|
||||
|
||||
其中 `data` 既可以传 JSON 对象,也可以传指向 JSON 文件的路径或 URL 字符串;`template` 支持本地路径或 http/https URL。数据契约校验由 `generate_report` 内部自动完成(不合法会报错)。
|
||||
|
||||
### 启动
|
||||
|
||||
```powershell
|
||||
# 安装后用 console script 启动
|
||||
lzwcai-mcpskills-generate-reports
|
||||
|
||||
# 或以模块方式运行
|
||||
python -m lzwcai_mcpskills_generate_reports.server
|
||||
```
|
||||
|
||||
### MCP 客户端配置示例
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"generate-reports": {
|
||||
"command": "lzwcai-mcpskills-generate-reports"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> stdio 模式下 stdout 被 MCP 协议占用,所有日志写入 `logs/` 目录与 stderr。
|
||||
|
||||
## 环境变量
|
||||
|
||||
| 变量 | 默认 | 说明 |
|
||||
|------|------|------|
|
||||
| `LOG_LEVEL` | `INFO` | 日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL)。 |
|
||||
| `LZWCAI_INSECURE_SSL` | 关闭 | 设为 `1`/`true`/`yes` 时,下载模板/数据/图片**关闭 SSL 证书校验**。仅用于内网自签名证书等可信场景,生产慎用。 |
|
||||
|
||||
## 数据契约(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": "面议"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- 必填顶层字段:`project_title`、`contact_person`、`contact_phone`、`requirements`。
|
||||
- `requirements` 传列表会自动拼成多行字符串。
|
||||
- `equipments[].index` 省略时自动从"四"起按中文数字补全(前三章固定为公司简介 / 客户要求 / 布局图)。
|
||||
- `params[].v` 允许为 `0`、空串等合法假值;仅当键缺失或值为 `null` 才算缺失。
|
||||
- 图片字段约定:路径/URL → 真实图;`""` → 默认占位图;`None` → 不显示。
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
lzwcai_mcpskills_generate_reports/
|
||||
├── pyproject.toml
|
||||
├── README.md
|
||||
├── main.py # 使用本包的示例脚本(仓库根,非包内)
|
||||
├── templates/ # 用户模板(示例,不在包内)
|
||||
│ └── standard/
|
||||
│ ├── template.docx
|
||||
│ └── meta.json
|
||||
├── samples/ # 示例数据(不在包内)
|
||||
│ └── sample_data.json
|
||||
└── lzwcai_mcpskills_generate_reports/ # Python 包
|
||||
├── __init__.py # 公共 API 入口
|
||||
├── main.py # 程序化便捷封装(URL data / out 可省略)
|
||||
├── server.py # MCP Server (stdio)
|
||||
├── pipeline.py # 总入口
|
||||
├── schema.py # 数据契约 + 校验器
|
||||
├── render_quote.py # 渲染引擎
|
||||
├── style_transfer.py # 样式迁移
|
||||
├── template_scanner.py # 模板占位符扫描
|
||||
└── utils/ # 下载 / 日志等工具
|
||||
```
|
||||
|
||||
## 模板约定
|
||||
|
||||
- 使用 [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,35 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
lzwcai-mcpskills-generate-reports
|
||||
|
||||
纯渲染引擎,不内置模板。
|
||||
对外暴露的公共 API:
|
||||
- generate: 数据 + 模板路径 -> docx(核心入口)
|
||||
- scan_template: 返回 QuoteData 数据契约结构(该传哪些字段、嵌套结构)
|
||||
- validate: 校验数据契约
|
||||
- normalize: 归一化数据
|
||||
- describe: 返回数据契约结构(scan_template 的底层实现)
|
||||
- transplant_style: 将用户模板样式迁移到结果文档
|
||||
"""
|
||||
|
||||
__version__ = "0.1.0"
|
||||
|
||||
from .pipeline import generate
|
||||
from .template_scanner import scan_template
|
||||
from .schema import validate, normalize, describe, DEFAULTS
|
||||
from .style_transfer import transplant_style
|
||||
from .utils.fetch import is_url, download_to_temp, local_file
|
||||
|
||||
__all__ = [
|
||||
"generate",
|
||||
"scan_template",
|
||||
"validate",
|
||||
"normalize",
|
||||
"describe",
|
||||
"transplant_style",
|
||||
"is_url",
|
||||
"download_to_temp",
|
||||
"local_file",
|
||||
"DEFAULTS",
|
||||
"__version__",
|
||||
]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
@@ -0,0 +1,78 @@
|
||||
2026-06-22 22:30:08 - lzwcai_mcpskills_generate_reports.server - ERROR - [server.py:166] - 工具执行失败: generate_report: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1019 (char 1018)
|
||||
Traceback (most recent call last):
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 55, in _load_data
|
||||
obj = json.loads(s)
|
||||
^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\json\__init__.py", line 346, in loads
|
||||
return _default_decoder.decode(s)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\json\decoder.py", line 337, in decode
|
||||
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\json\decoder.py", line 355, in raw_decode
|
||||
raise JSONDecodeError("Expecting value", s, err.value) from None
|
||||
json.decoder.JSONDecodeError: Expecting value: line 1 column 1019 (char 1018)
|
||||
|
||||
During handling of the above exception, another exception occurred:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 163, in handle_call_tool
|
||||
result = await anyio.to_thread.run_sync(handler, args)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\site-packages\anyio\to_thread.py", line 63, in run_sync
|
||||
return await get_async_backend().run_sync_in_worker_thread(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 2502, in run_sync_in_worker_thread
|
||||
return await future
|
||||
^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 986, in run
|
||||
result = context.run(func, *args)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 120, in _do_generate_report
|
||||
return _generate_report(
|
||||
^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 99, in generate_report
|
||||
data = _load_data(data)
|
||||
^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 57, in _load_data
|
||||
raise ValueError(f"data 看起来是 JSON 内容但解析失败: {e}")
|
||||
ValueError: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1019 (char 1018)
|
||||
2026-06-22 22:30:32 - lzwcai_mcpskills_generate_reports.server - ERROR - [server.py:166] - 工具执行失败: generate_report: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1012 (char 1011)
|
||||
Traceback (most recent call last):
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 55, in _load_data
|
||||
obj = json.loads(s)
|
||||
^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\json\__init__.py", line 346, in loads
|
||||
return _default_decoder.decode(s)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\json\decoder.py", line 337, in decode
|
||||
obj, end = self.raw_decode(s, idx=_w(s, 0).end())
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\json\decoder.py", line 355, in raw_decode
|
||||
raise JSONDecodeError("Expecting value", s, err.value) from None
|
||||
json.decoder.JSONDecodeError: Expecting value: line 1 column 1012 (char 1011)
|
||||
|
||||
During handling of the above exception, another exception occurred:
|
||||
|
||||
Traceback (most recent call last):
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 163, in handle_call_tool
|
||||
result = await anyio.to_thread.run_sync(handler, args)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\site-packages\anyio\to_thread.py", line 63, in run_sync
|
||||
return await get_async_backend().run_sync_in_worker_thread(
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 2502, in run_sync_in_worker_thread
|
||||
return await future
|
||||
^^^^^^^^^^^^
|
||||
File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 986, in run
|
||||
result = context.run(func, *args)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 120, in _do_generate_report
|
||||
return _generate_report(
|
||||
^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 99, in generate_report
|
||||
data = _load_data(data)
|
||||
^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 57, in _load_data
|
||||
raise ValueError(f"data 看起来是 JSON 内容但解析失败: {e}")
|
||||
ValueError: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1012 (char 1011)
|
||||
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
main.py:程序化调用入口(非 CLI、非 MCP server)。
|
||||
|
||||
两点特性:
|
||||
1. template / data 既可传本地文件路径,也可传 http/https URL(自动下载)。
|
||||
2. 输出路径 out 可省略;省略时落到当前目录下的 _out/,
|
||||
文件名按 模板名 + 时间戳 自动生成。
|
||||
|
||||
用法:
|
||||
from lzwcai_mcpskills_generate_reports.main import generate_report, scan_report
|
||||
|
||||
# data 传 dict
|
||||
generate_report(template="./模板.docx", data={...})
|
||||
# data 传本地 JSON 路径,out 省略
|
||||
generate_report(template="./模板.docx", data="data.json")
|
||||
# template / data 都传 URL
|
||||
generate_report(
|
||||
template="https://host/模板.docx",
|
||||
data="https://host/data.json",
|
||||
out="_out/a.docx",
|
||||
)
|
||||
# 扫描占位符,支持本地路径或 URL
|
||||
scan_report(template="https://host/模板.docx")
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
try:
|
||||
sys.stdout.reconfigure(encoding="utf-8")
|
||||
except (AttributeError, OSError):
|
||||
pass
|
||||
|
||||
from . import generate, scan_template
|
||||
from .utils.fetch import is_url, local_file
|
||||
|
||||
|
||||
def _load_data(data):
|
||||
"""把 data 归一化为 dict:支持 dict、JSON 内容字符串、本地 JSON 文件路径、或 JSON 文件 URL。
|
||||
|
||||
字符串的判定顺序:
|
||||
1. 以 '{' 开头 -> 当作 JSON 内容直接解析;
|
||||
2. 否则尝试 JSON 解析(捕获失败则当作路径);
|
||||
3. 本地路径 / URL(URL 会下载到临时文件读取,用完即删)。
|
||||
"""
|
||||
if isinstance(data, dict):
|
||||
return data
|
||||
if isinstance(data, str):
|
||||
s = data.strip()
|
||||
# 1) 看起来就是 JSON 对象内容(路径/URL 不会以 '{' 开头)
|
||||
if s.startswith("{"):
|
||||
try:
|
||||
obj = json.loads(s)
|
||||
except json.JSONDecodeError as e:
|
||||
raise ValueError(f"data 看起来是 JSON 内容但解析失败: {e}")
|
||||
if not isinstance(obj, dict):
|
||||
raise TypeError("data 为 JSON 内容时必须是对象(dict)")
|
||||
return obj
|
||||
# 2) 尝试当作 JSON 字符串解析(捕获失败说明是路径)
|
||||
try:
|
||||
obj = json.loads(s)
|
||||
if isinstance(obj, dict):
|
||||
return obj
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
# 3) 本地路径 / URL
|
||||
with local_file(data, suffix=".json") as path:
|
||||
if not is_url(data) and not os.path.isfile(path):
|
||||
raise FileNotFoundError(f"数据文件不存在: {data}")
|
||||
with open(path, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
raise TypeError(
|
||||
f"data 必须是 dict、JSON 内容/文件路径/URL 字符串,实际类型: {type(data).__name__}"
|
||||
)
|
||||
|
||||
|
||||
def _default_out_path(template):
|
||||
"""out 省略时的默认输出路径:<cwd>/_out/<模板名>_<时间戳>.docx。"""
|
||||
name = os.path.basename(template.split("?")[0]) # 去掉 URL 查询串
|
||||
base = os.path.splitext(name)[0] or "report"
|
||||
stamp = time.strftime("%Y%m%d_%H%M%S")
|
||||
return os.path.join(os.getcwd(), "_out", f"{base}_{stamp}.docx")
|
||||
|
||||
|
||||
def generate_report(template, data, out=None, style_ref=None):
|
||||
"""生成 docx 报告。
|
||||
|
||||
参数:
|
||||
template: 模板 docx 文件路径,或 http/https URL(自动下载)
|
||||
data: dict、JSON 文件路径,或 JSON 文件 URL(自动下载)
|
||||
out: 输出 docx 文件路径;省略则用默认路径(_out/ 下按模板名+时间戳命名)
|
||||
style_ref: 用户样式参考 docx 路径或 URL(可选)
|
||||
|
||||
返回:
|
||||
dict: {"output": 输出文件绝对路径}
|
||||
"""
|
||||
data = _load_data(data)
|
||||
if not out:
|
||||
out = _default_out_path(template)
|
||||
out_path = generate(data=data, template=template, out_path=out, style_ref=style_ref)
|
||||
return {"output": out_path}
|
||||
|
||||
|
||||
def scan_report(template=None):
|
||||
"""返回 QuoteData 数据契约结构(该传哪些字段、是否必填、嵌套结构)。
|
||||
|
||||
参数:
|
||||
template: 已忽略,保留兼容旧签名。
|
||||
|
||||
返回:
|
||||
dict: 数据契约,见 schema.describe()。
|
||||
"""
|
||||
return scan_template(template)
|
||||
@@ -0,0 +1,91 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
pipeline.py:总入口 — 串起 数据校验 -> 渲染 -> (可选)样式迁移。
|
||||
|
||||
注意:本包不维护内置模板。调用方需要自己准备 .docx 模板文件,
|
||||
并通过 template 参数传入路径。
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import contextlib
|
||||
|
||||
from .schema import validate, normalize
|
||||
from .render_quote import render
|
||||
from .style_transfer import transplant_style
|
||||
from .utils.fetch import is_url, local_file
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def _noop():
|
||||
"""style_ref 为空时占位用的空上下文,产出 None。"""
|
||||
yield None
|
||||
|
||||
|
||||
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 文件路径,或 http/https URL(自动下载)
|
||||
out_path: 输出 docx 路径
|
||||
style_ref: 用户上传的样式参考 docx 路径,或 URL(可选,自动下载)
|
||||
|
||||
返回:
|
||||
生成的 docx 绝对路径
|
||||
"""
|
||||
data = _load_data(data)
|
||||
|
||||
if not isinstance(template, str):
|
||||
raise TypeError(f"template 必须是文件路径或 URL 字符串,实际类型: {type(template).__name__}")
|
||||
|
||||
# 归一化 + 校验
|
||||
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)
|
||||
|
||||
# 把 template / style_ref 统一解析成本地路径(URL 会下载到临时文件,用完即删)
|
||||
with local_file(template) as template_path, \
|
||||
(local_file(style_ref) if style_ref else _noop()) as style_path:
|
||||
if not is_url(template) and not os.path.isfile(template_path):
|
||||
raise FileNotFoundError(f"模板文件不存在: {template}")
|
||||
template_path = os.path.abspath(template_path)
|
||||
|
||||
# 渲染(优先读取同目录 meta.json 作为图片配置;URL 模板无同目录 meta,则为空)
|
||||
meta = _load_meta_for_template(template_path)
|
||||
render(data, out_path, template_path, meta=meta)
|
||||
|
||||
# 可选样式迁移
|
||||
if style_ref:
|
||||
if not is_url(style_ref) and not os.path.isfile(style_path):
|
||||
raise FileNotFoundError(f"样式参考文件不存在: {style_ref}")
|
||||
transplant_style(out_path, style_path, out_path)
|
||||
|
||||
return os.path.abspath(out_path)
|
||||
@@ -0,0 +1,160 @@
|
||||
# -*- 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 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
|
||||
|
||||
from .utils.fetch import make_ssl_context
|
||||
|
||||
|
||||
def _resolve_image_path(src, tmp_files):
|
||||
"""把图片字段值解析为本地文件路径。
|
||||
|
||||
下载成功的临时文件会记录到 tmp_files,由调用方在渲染结束后统一清理。
|
||||
"""
|
||||
def _download(url):
|
||||
try:
|
||||
ctx = make_ssl_context()
|
||||
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,194 @@
|
||||
# -*- 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
|
||||
# 用"键是否存在/为空"判断,避免把合法假值(0、False、空串当 v)误判为缺失
|
||||
if not p.get("k"):
|
||||
errors.append(f"equipments[{i}].params[{j}] 缺少 k")
|
||||
if "v" not in p or p.get("v") is None:
|
||||
errors.append(f"equipments[{i}].params[{j}] 缺少 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
|
||||
|
||||
|
||||
def describe():
|
||||
"""返回 QuoteData 数据契约结构(该传哪些字段、是否必填、嵌套结构)。
|
||||
|
||||
与 validate / normalize 保持一致,是「调用方该如何组织数据」的权威说明,
|
||||
可直接对照 samples/sample_data.json。返回新副本,调用方可安全修改。
|
||||
"""
|
||||
return {
|
||||
"type": "QuoteData",
|
||||
"required": ["project_title", "contact_person", "contact_phone", "requirements"],
|
||||
"fields": {
|
||||
"project_title": {"type": "string", "required": True, "desc": "项目/方案标题"},
|
||||
"contact_person": {"type": "string", "required": True, "desc": "联系人"},
|
||||
"contact_phone": {"type": "string", "required": True, "desc": "联系电话"},
|
||||
"contact_company": {"type": "string", "required": False, "desc": "客户公司名(模板用到才生效)"},
|
||||
"requirements": {
|
||||
"type": "string | list[string]",
|
||||
"required": True,
|
||||
"desc": "客户要求;传列表会自动拼成多行字符串",
|
||||
},
|
||||
"layout_image": {"type": "string", "required": False, "desc": "整线布局图,本地路径或 URL;空串=占位图,None=不显示"},
|
||||
"layout_title": {"type": "string", "required": False, "default": DEFAULTS["layout_title"]},
|
||||
"show_layout": {"type": "bool", "required": False, "default": DEFAULTS["show_layout"]},
|
||||
"equipments": {
|
||||
"type": "list",
|
||||
"required": False,
|
||||
"desc": "设备清单",
|
||||
"item": {
|
||||
"name": {"type": "string", "required": True, "desc": "设备名称"},
|
||||
"index": {"type": "string", "required": False, "desc": "章节序号,缺省自动按中文数字(四、五…)补全"},
|
||||
"images": {"type": "list[string]", "required": False, "desc": "设备图,路径或 URL 列表;缺省为 ['']"},
|
||||
"features": {
|
||||
"type": "list",
|
||||
"required": False,
|
||||
"item": {
|
||||
"title": {"type": "string", "required": True, "desc": "特点标题"},
|
||||
"lines": {"type": "list[string]", "required": True, "desc": "特点说明,多行"},
|
||||
},
|
||||
},
|
||||
"params": {
|
||||
"type": "list",
|
||||
"required": False,
|
||||
"item": {
|
||||
"k": {"type": "string", "required": True, "desc": "参数名"},
|
||||
"v": {"type": "string | number", "required": True, "desc": "参数值,允许 0/空串等合法假值"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"quote_items": {
|
||||
"type": "list",
|
||||
"required": False,
|
||||
"desc": "报价表条目",
|
||||
"item": {
|
||||
"name": {"type": "string", "required": True, "desc": "条目名称"},
|
||||
"qty": {"type": "string", "required": False, "desc": "数量,如 '1套'"},
|
||||
"image": {"type": "string", "required": False, "desc": "条目图,路径或 URL;缺省为空串"},
|
||||
"desc": {"type": "string", "required": False, "desc": "条目说明"},
|
||||
"price": {"type": "string", "required": False, "desc": "价格,如 '面议'"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"auto_generated": {
|
||||
"section_quote_table": "报价表章节序号,按设备数量自动计算,无需提供",
|
||||
"section_after_sales": "售后服务章节序号,按设备数量自动计算,无需提供",
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,205 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
lzwcai-mcpskills-generate-reports MCP Server
|
||||
|
||||
把 docx 模板渲染引擎封装成 MCP 工具,提供两个工具:
|
||||
- generate_report: 数据 + 模板路径 -> 渲染输出 docx
|
||||
- scan_template: 返回 QuoteData 数据契约结构(该传哪些字段、嵌套结构)
|
||||
|
||||
stdio 模式运行;所有日志走 stderr,stdout 留给 MCP 协议。
|
||||
"""
|
||||
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 scan_template
|
||||
from .main import generate_report as _generate_report
|
||||
except ImportError:
|
||||
from lzwcai_mcpskills_generate_reports.utils.logger_config import (
|
||||
setup_system_logging, get_logger,
|
||||
)
|
||||
from lzwcai_mcpskills_generate_reports import scan_template
|
||||
from lzwcai_mcpskills_generate_reports.main import (
|
||||
generate_report as _generate_report,
|
||||
)
|
||||
|
||||
# 初始化日志系统
|
||||
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")
|
||||
|
||||
|
||||
# ── 工具定义 ──────────────────────────────────────────────
|
||||
_DATA_DESC = "报价数据:可以是 JSON 对象,或指向 JSON 文件的路径/URL 字符串"
|
||||
|
||||
TOOL_DEFS = [
|
||||
types.Tool(
|
||||
name="generate_report",
|
||||
description=(
|
||||
"用 docx 模板 + 结构化数据渲染生成报价文档 docx,返回输出文件绝对路径。"
|
||||
"模板由调用方提供,本工具不内置模板。"
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"template": {
|
||||
"type": "string",
|
||||
"description": "模板 docx 文件路径,或 http/https URL(会自动下载)(必填)",
|
||||
},
|
||||
"data": {
|
||||
"type": "string",
|
||||
"description": _DATA_DESC + "(必填)",
|
||||
},
|
||||
"out": {
|
||||
"type": "string",
|
||||
"description": "输出 docx 文件路径(可选);省略则落到当前目录 _out/,按 模板名_时间戳.docx 自动命名",
|
||||
},
|
||||
"style_ref": {
|
||||
"type": "string",
|
||||
"description": "用户上传的样式参考 docx 路径或 URL(可选),会把其 theme/字体套到结果文档",
|
||||
},
|
||||
},
|
||||
"required": ["template", "data"],
|
||||
},
|
||||
# 输出契约:成功返回 {"output": 路径};失败返回 {"error":..., "tool_name":...}。
|
||||
# MCP 要求 outputSchema 顶层必须是 type:"object",所以 oneOf 只用来约束
|
||||
# 两种 required 组合,否则出错时 structuredContent 会过不了校验。
|
||||
outputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"output": {"type": "string", "description": "输出 docx 文件绝对路径"},
|
||||
"error": {"type": "string", "description": "错误信息"},
|
||||
"tool_name": {"type": "string"},
|
||||
},
|
||||
"oneOf": [
|
||||
{"required": ["output"]},
|
||||
{"required": ["error", "tool_name"]},
|
||||
],
|
||||
},
|
||||
),
|
||||
types.Tool(
|
||||
name="scan_template",
|
||||
description=(
|
||||
"返回 QuoteData 数据契约结构:该传哪些字段、是否必填、equipments/features/params/"
|
||||
"quote_items 等嵌套结构,以及自动生成无需提供的字段。用于在渲染前了解数据该如何组织。"
|
||||
),
|
||||
inputSchema={
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"template": {
|
||||
"type": "string",
|
||||
"description": "(已忽略,保留兼容)模板路径或 URL;所有模板共用同一数据契约",
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
},
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@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:
|
||||
# 复用 main.generate_report:data 支持 URL,out 可省略(落到 _out/ 自动命名)
|
||||
return _generate_report(
|
||||
template=arguments["template"],
|
||||
data=arguments["data"],
|
||||
out=arguments.get("out"),
|
||||
style_ref=arguments.get("style_ref"),
|
||||
)
|
||||
|
||||
|
||||
def _do_scan_template(arguments: dict) -> dict:
|
||||
return scan_template(arguments.get("template"))
|
||||
|
||||
|
||||
_HANDLERS = {
|
||||
"generate_report": _do_generate_report,
|
||||
"scan_template": _do_scan_template,
|
||||
}
|
||||
|
||||
|
||||
@server.call_tool()
|
||||
async def handle_call_tool(
|
||||
name: str,
|
||||
arguments: dict | None,
|
||||
) -> tuple[list[types.TextContent], dict]:
|
||||
"""调用工具。
|
||||
|
||||
返回 (content, structured) 二元组:
|
||||
- structured: 结构化结果 dict,SDK 自动填入响应的 structuredContent,
|
||||
调用方可直接取值,无需再对 content[].text 做 json.loads。
|
||||
- content: 序列化后的 TextContent,保留对老客户端的向后兼容。
|
||||
"""
|
||||
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}
|
||||
|
||||
content = [
|
||||
types.TextContent(
|
||||
type="text",
|
||||
text=json.dumps(result, ensure_ascii=False, indent=2),
|
||||
)
|
||||
]
|
||||
return content, result
|
||||
|
||||
|
||||
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,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
template_scanner.py:返回 QuoteData 数据契约结构。
|
||||
|
||||
历史上本模块扫描 docx 模板里的 Jinja2 占位符;现已改为直接返回
|
||||
schema.describe() 的数据契约(该传哪些字段、是否必填、嵌套结构),
|
||||
更直观、可直接对照 samples/sample_data.json。
|
||||
|
||||
保留 scan_template 这个名字与 template 参数,向后兼容既有调用方。
|
||||
"""
|
||||
from .schema import describe
|
||||
|
||||
|
||||
def scan_template(template_path=None):
|
||||
"""返回 QuoteData 数据契约结构。
|
||||
|
||||
参数:
|
||||
template_path: 兼容旧签名,已忽略(所有模板共用同一数据契约)。
|
||||
|
||||
返回:
|
||||
dict: 数据契约,见 schema.describe()。
|
||||
"""
|
||||
return describe()
|
||||
@@ -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,77 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
fetch.py:把远程文件地址(http/https)下载成本地临时文件。
|
||||
|
||||
用于让 generate / scan_template 既能接收本地路径,也能直接接收一个 URL,
|
||||
下载后按本地文件处理,用完即删。
|
||||
"""
|
||||
import contextlib
|
||||
import os
|
||||
import ssl
|
||||
import tempfile
|
||||
import urllib.request
|
||||
|
||||
|
||||
def is_url(s):
|
||||
"""判断字符串是否是 http/https URL。"""
|
||||
return isinstance(s, str) and s.lower().startswith(("http://", "https://"))
|
||||
|
||||
|
||||
def make_ssl_context():
|
||||
"""构造下载用的 SSL 上下文。
|
||||
|
||||
默认开启证书校验(安全)。仅当环境变量 LZWCAI_INSECURE_SSL 设为
|
||||
1/true/yes 时才关闭校验,用于内网自签名证书等可信场景。
|
||||
"""
|
||||
ctx = ssl.create_default_context()
|
||||
if os.environ.get("LZWCAI_INSECURE_SSL", "").lower() in ("1", "true", "yes"):
|
||||
ctx.check_hostname = False
|
||||
ctx.verify_mode = ssl.CERT_NONE
|
||||
return ctx
|
||||
|
||||
|
||||
def download_to_temp(url, suffix=".docx"):
|
||||
"""下载 URL 内容到临时文件,返回本地路径。
|
||||
|
||||
参数:
|
||||
url: 远程文件地址(http/https)
|
||||
suffix: 当 URL 末尾无扩展名时使用的默认后缀
|
||||
|
||||
返回:
|
||||
本地临时文件绝对路径(调用方负责删除)
|
||||
"""
|
||||
ctx = make_ssl_context()
|
||||
req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"})
|
||||
data = urllib.request.urlopen(req, context=ctx, timeout=60).read()
|
||||
|
||||
ext = os.path.splitext(url.split("?")[0])[1] or suffix
|
||||
fd, path = tempfile.mkstemp(suffix=ext)
|
||||
try:
|
||||
os.write(fd, data)
|
||||
finally:
|
||||
os.close(fd)
|
||||
return path
|
||||
|
||||
|
||||
@contextlib.contextmanager
|
||||
def local_file(path_or_url, suffix=".docx"):
|
||||
"""统一把"本地路径或 URL"解析成本地路径的上下文管理器。
|
||||
|
||||
- 传入本地路径:原样产出,退出时不删除。
|
||||
- 传入 URL:下载到临时文件并产出其路径,退出时自动删除。
|
||||
|
||||
用法:
|
||||
with local_file(template) as path:
|
||||
... 用 path 读模板 ...
|
||||
"""
|
||||
if is_url(path_or_url):
|
||||
tmp = download_to_temp(path_or_url, suffix=suffix)
|
||||
try:
|
||||
yield tmp
|
||||
finally:
|
||||
try:
|
||||
os.remove(tmp)
|
||||
except OSError:
|
||||
pass
|
||||
else:
|
||||
yield path_or_url
|
||||
@@ -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)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user