diff --git a/.kilo/skills/lzwcai-agile-db.zip b/.kilo/skills/lzwcai-agile-db.zip deleted file mode 100644 index 72157c8..0000000 Binary files a/.kilo/skills/lzwcai-agile-db.zip and /dev/null differ diff --git a/.kilo/skills/lzwcai-agile-db/SKILL.md b/.kilo/skills/lzwcai-agile-db/SKILL.md index 16f04f9..ac1b2ec 100644 --- a/.kilo/skills/lzwcai-agile-db/SKILL.md +++ b/.kilo/skills/lzwcai-agile-db/SKILL.md @@ -1,7 +1,8 @@ --- name: lzwcai-agile-db description: AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。 -version: 0.2.0 +metadata: + version: 0.4.2 --- # 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 个工具) | 工具 | 功能 | 危险等级 | |------|------|----------| @@ -74,8 +98,9 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 | `delete_api_key` | 删除 API 密钥 | **危险** | | `get_api_key_permissions` | 查看指定密钥的权限配置 | 安全 | | `grant_api_key_permissions` | 批量为 API 密钥授予权限 | **危险** | +| `revoke_api_key_permissions` | 撤销/删除已授予的权限(按权限记录 ID) | **危险** | -### 八、技能与工具管理(6 个工具) +### 九、技能与工具管理(7 个工具) | 工具 | 功能 | 危险等级 | |------|------|----------| @@ -84,7 +109,29 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 | `create_skill` | 为数据源创建技能 | 中等 | | `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 +150,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,6 +170,20 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 ## ⚠️ 安全确认原则(必须遵守) +### 〇、目标资源不得擅自选择(最高优先级铁律) + +定位操作目标的参数(`datasourceId` / `connectionId` / `databaseName` / `tableId` / `tableName` / `apiKeyId` / `skillId` 等),**永远不得猜测、编造或"随手挑一个"**: + +- **多个候选** → 列出全部,让用户选;严禁默认第一个或自认为"最合理"的那个。 +- **唯一候选** → 先声明"我将使用 XXX",给用户否决机会;创建/写入/删除类仍需用户确认后执行。 +- **没有候选 / 信息不足** → 直接问用户,不得编造。 + +> 当你发现自己正在"替用户决定用哪个数据源/数据库/表"时,立刻停下来改成提问。 + +**创建类操作**(`create_table` / `create_datasource` 等)执行前必须逐项确认落点,绝不"随便找个数据源就建"。例如建表须确认:数据源(`connectionId`)+ 数据库(`databaseName`)+ 表名(`tableName`),三者都要用户点头。 + +--- + ### 一、执行工具前的风险评估 当执行以下类型的操作时,**必须先询问用户确认**,不得擅作主张: @@ -294,26 +358,36 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 **用户**: "查一下 users 表前 10 条数据" ``` -调用: 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 +403,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 +419,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 +436,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),必须二次确认 @@ -497,7 +572,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 - `data` 只包含要更新的字段,不需要提供全部字段 - 插入数据时,自增主键不需要提供 - 如果操作涉及多行,使用批量操作或循环调用 -- 增删改操作默认作用于 `prod` 环境 +- 增删改操作 `target` **默认 `test`**(安全优先);要写正式数据必须显式传 `target="prod"` --- @@ -511,12 +586,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 +610,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 +648,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 databaseName="order_db", tableName="products", tableComment="商品表", - columns=[...] // 使用 AI 生成的 columns + columns=[...] // 使用轮询拿到的 createTableData.data.tables[0].columns ) 回复: 已成功创建表 products (商品表) ``` @@ -580,15 +669,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 +695,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,9 +705,10 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 说明:导入的数据将写入数据库,请确认数据来源合法合规,不包含敏感信息、政治内容或违规内容。 请确认是否继续? ↓ -5. 用户确认无误后,调用 confirm_import_data(connectionId="xx", data={...}, target="test") +5. 用户确认无误后,调用 confirm_import_data(connectionId="xx", databaseName="目标库名", data=<预览返回的原文>, target="test") + (把 preview_import_data 的返回原样传给 data,工具会自动组装后端要求的结构;databaseName 是落库的目标库) ↓ -6. 确认导入成功 +6. 确认导入成功(返回含 insertedRows 插入行数) ``` ### 注意事项 @@ -628,7 +716,10 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 - 支持格式:.xlsx / .xls - 导入前默认使用 `test` 环境(安全做法) - 如果用户要导入到正式环境,必须二次确认 -- base64 编码的文件内容需要提供文件名 +- `file_url` 要求 http/https 可下载链接,且文件大小 < 500KB +- `file_url` 对应的文件扩展名需为 `.xlsx` / `.xls` +- **`confirm_import_data` 必须传 `databaseName`**(落库目标库);`data` 直接传 `preview_import_data` 的返回原文即可,工具内部会自动解包并组装成 `{tableStructure(含databaseName), allData}` +- AI 识别会把中文表头转成英文列名(如「姓名」→`name`);若导入数据键名与生成的列名对不上会报「未找到 XX 字段」,此时需按预览返回的列名核对 --- @@ -707,6 +798,23 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 - `database`:数据库级别权限 - `table`:表级别权限 +### 7.7 撤销权限 + +``` +调用: get_api_key_permissions(apiKeyId="7") +返回: { + "data": { + "connectionPermissions": [{"id": "101", "connectionId": "58", "permissionType": "read"}], + "databasePermissions": [...], + "tablePermissions": [...] + } +} + +调用: revoke_api_key_permissions(permissionIds=["101"]) +``` + +> 说明:`revoke_api_key_permissions` 按权限记录的 `id` 删除,需先从 `get_api_key_permissions` 获取。 + --- ## 场景 8:技能与工具管理 @@ -768,6 +876,20 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 ) ``` +### 8.7 修改技能下某个工具 + +``` +调用: update_skill_tool( + id="2066468871151718402", // 工具 ID(来自 get_skill_tools 返回的 id) + description="改后的业务描述", // 可选 + uniqueName="新展示名", // 可选(工具展示名) + sqlTemplate="SELECT ...", // 可选 + resultType="list" // 可选:single / list +) +``` + +> 说明:后端实体真实字段为 `id`/`uniqueName`/`description`/`sqlTemplate`/`resultType`。旧参数名 `skillToolId`/`name`/`businessDescription` 仍有兼容映射,但建议直接用上面的真实字段名。`businessScenario` 后端无此字段,会被丢弃。 + --- ## 场景 9:表订阅管理 @@ -778,13 +900,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 +1065,8 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 - `1` - 已停止 ### 环境 -- `prod` - 生产环境(默认) -- `test` - 测试环境 +- `test` - 测试环境(**表数据 CRUD 的默认值**,安全优先) +- `prod` - 生产环境(操作正式数据须显式传 `target="prod"`;仅 `execute_sql` 默认 prod) ### 常用字段类型 | 类型 | 用途 | 示例 | @@ -854,16 +1080,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 +1102,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 如果用户说"帮我查一下数据库",按以下步骤操作: 1. 调用 `list_datasources()` 获取数据源列表 -2. 展示列表,让用户选择或默认第一个运行中的数据源 +2. 展示列表,让用户选择目标数据源(**多个候选时必须让用户选,不得默认第一个**;只有一个候选也要先说明"我将使用 XXX"再继续) 3. 调用 `get_datasource_detail(datasourceId="xx")` 获取数据库和表信息 4. 引导用户选择要操作的表 5. 根据用户意图调用相应的工具: diff --git a/.kilo/skills/mcp-tool-testing.zip b/.kilo/skills/mcp-tool-testing.zip deleted file mode 100644 index 5303d59..0000000 Binary files a/.kilo/skills/mcp-tool-testing.zip and /dev/null differ diff --git a/lzwcai_mcp_agile_db/README.md b/lzwcai_mcp_agile_db/README.md index 8ae50f6..c996cd7 100644 --- a/lzwcai_mcp_agile_db/README.md +++ b/lzwcai_mcp_agile_db/README.md @@ -1,6 +1,6 @@ # lzwcai-mcp-agile-db -数据库管理平台 MCP Server,提供 33 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。 +数据库管理平台 MCP Server,提供 34 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。 ## 环境变量 @@ -58,6 +58,7 @@ lzwcai-mcp-agile-db - `delete_api_key` - 删除密钥 - `get_api_key_permissions` - 查看密钥权限 - `grant_api_key_permissions` - 授予权限 +- `revoke_api_key_permissions` - 撤销/删除已授予权限 ### 技能与工具管理 - `get_skill_by_datasource` - 获取技能信息 diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/__init__.cpython-312.pyc index a0a9bb2..c958bb3 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/__init__.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/server.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/server.cpython-312.pyc index 9caadc5..5294a1b 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/server.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/server.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db.log b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db.log index 1379807..f62c1f9 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db.log +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db.log @@ -1,202 +1,10 @@ -2026-06-11 09:30:49 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs -2026-06-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: -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: -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: -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= -2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context= 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= -2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request= -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= -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= -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= -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= -2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context= 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= -2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request= -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= -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= -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= -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: -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: -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= -2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context= 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= -2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request= -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= -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= -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= -2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete -2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started -2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete -2026-06-11 09:40:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200 -2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_datasources -2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: { - "total": 14, - "rows": [ - { - "createBy": "", - "createTime": "2026-06-10 16:47:43", - "updateBy": "", - "updateTime": "2026-06-10 16:47:43", - "remark": "设备报价管理系统包含设备基础信息、预设方案模板、报价单主表和明细表四个核心数据对象,支持从设备参数管理到整套产线方案配置的完整报价流程,为销售部门提供标准化报价服务,实现快速方案生成和精准成本核算。", - "id": "58", - "enterpriseId": "1937166012193443842", - "deptId": "1171", - "userId": "292", - "host": "host.docker.internal", - "port": 5432, - "datasourceName": "HMD产品", - "database... -2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent -2026-06-11 09:40:55 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: -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= -2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context= 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= -2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request= -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= -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= -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= -2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete -2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started -2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete -2026-06-11 09:40:56 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200 -2026-06-11 09:40:56 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys -2026-06-11 09:40:56 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: { - "total": 6, - "rows": [ - { - "createBy": "", - "createTime": "2026-06-06 15:10:31", - "updateBy": "", - "updateTime": "2026-06-06 15:10:31", - "remark": null, - "id": "7", - "apiKey": "Lb8LgEJ7eBUU8QMifKUJvo9w6YLAotbKJ-w1DKU8ZrU", - "apiKeyName": "AWINBEXT", - "enterpriseId": "1937166012193443842", - "status": 0, - "expireTime": "2027-06-06T15:10:32.000+08:00" - }, - { - "createBy": "", - "createTime": "2026-05-25 14:47:11", - "u... -2026-06-11 09:40:56 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-17 11:19:35 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs +2026-06-17 11:19:35 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db' +2026-06-17 11:19:35 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-17 11:19:35 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=http://x +2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:78] - [API响应] HTTP 400 +2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:98] - [API错误] HTTP 400, 参数不合法 +2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:78] - [API响应] HTTP 200 +2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:78] - [API响应] HTTP 404 +2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:107] - [API错误] HTTP 404, {"path": "/x"} diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db_error.log b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db_error.log index fa261fd..eb8d141 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db_error.log +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db_error.log @@ -1,15 +1,2 @@ -2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:91] - [API错误] 登录过期,请重新登录 -2026-06-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-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:98] - [API错误] HTTP 400, 参数不合法 +2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:107] - [API错误] HTTP 404, {"path": "/x"} diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py index 035da04..4f144cf 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py @@ -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.12", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), ), - ), - ) + ) + finally: + # 释放全局 HTTP 客户端 + if _api_client is not None: + await _api_client.close() def main(): diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/__init__.cpython-312.pyc index a9dadb2..0cf7a27 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/__init__.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/_base.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/_base.cpython-312.pyc index 282110b..836e61d 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/_base.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/_base.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/ai_training.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/ai_training.cpython-312.pyc new file mode 100644 index 0000000..5bb2392 Binary files /dev/null and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/ai_training.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/api_keys.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/api_keys.cpython-312.pyc index bf3dd89..db7da02 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/api_keys.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/api_keys.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/connection_config.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/connection_config.cpython-312.pyc new file mode 100644 index 0000000..856d041 Binary files /dev/null and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/connection_config.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/data_import.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/data_import.cpython-312.pyc index e15c973..0c41287 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/data_import.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/data_import.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/database_tables.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/database_tables.cpython-312.pyc index 8f85dc8..7afa7e3 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/database_tables.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/database_tables.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/datasources.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/datasources.cpython-312.pyc index 0e71be6..303eb14 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/datasources.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/datasources.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/mqtt_sync.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/mqtt_sync.cpython-312.pyc new file mode 100644 index 0000000..4fa5eda Binary files /dev/null and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/mqtt_sync.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/skills.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/skills.cpython-312.pyc index 8a0ffae..bda77d2 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/skills.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/skills.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/sql_execution.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/sql_execution.cpython-312.pyc index c327e76..3edc2fc 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/sql_execution.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/sql_execution.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/subscriptions.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/subscriptions.cpython-312.pyc index f88c827..2b87bc8 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/subscriptions.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/subscriptions.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/table_data.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/table_data.cpython-312.pyc index cde5aac..a0cc166 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/table_data.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/table_data.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/ai_training.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/ai_training.py new file mode 100644 index 0000000..96dd9b6 --- /dev/null +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/ai_training.py @@ -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']}") diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py index 16b29c5..31eb772 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py @@ -1,5 +1,5 @@ """ -API 密钥管理工具 (工具 18-23) +API 密钥管理工具(含创建、状态切换、删除、权限查询/授予/撤销) """ from ._base import register_tool, ToolDef @@ -119,3 +119,34 @@ class GrantApiKeyPermissionsTool(ToolDef): async def execute(self, args: dict) -> dict: return await self.client.post("/datasource/api_key/permission/grant_batch", json_data=args) + + +@register_tool("revoke_api_key_permissions") +class RevokeApiKeyPermissionsTool(ToolDef): + name = "revoke_api_key_permissions" + description = "撤销/删除 API 密钥已授予的权限(按权限记录 ID)" + input_schema = { + "type": "object", + "properties": { + "permissionIds": { + "type": "array", + "items": {"type": "string"}, + "description": ( + "权限记录 ID 列表。" + "先从 get_api_key_permissions 获取," + "取 connectionPermissions / databasePermissions / tablePermissions 中每项的 id 字段" + ), + }, + }, + "required": ["permissionIds"], + } + + async def execute(self, args: dict) -> dict: + args = dict(args) + permission_ids = args.pop("permissionIds", None) or [] + # 过滤掉空字符串/None,防止拼接出类似 "1,,2" 的非法 ID + permission_ids = [pid for pid in permission_ids if pid is not None and str(pid).strip()] + if not permission_ids: + raise ValueError("permissionIds 不能为空") + ids = ",".join(str(pid).strip() for pid in permission_ids) + return await self.client.delete(f"/datasource/api_key/permission/{ids}") diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/connection_config.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/connection_config.py new file mode 100644 index 0000000..1331d0f --- /dev/null +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/connection_config.py @@ -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"]}) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py index 26c2478..7ea63f3 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py @@ -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,62 +13,186 @@ 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") - - # 解码 base64 文件 - file_content = base64.b64decode(file_base64) - - # 构建文件上传 + file_url = args.pop("file_url") + + # 0. 基础 URL 校验 + parsed_url = urlparse(file_url) + if parsed_url.scheme not in ("http", "https"): + raise ValueError("仅支持 http/https 协议的下载地址") + + # 1. 下载文件 + download_resp = await self.client.client.get(file_url, follow_redirects=True, timeout=30.0) + download_resp.raise_for_status() + + # 1.1 预先根据 Content-Length 拦截超大文件,避免无意义下载 + max_size = 500 * 1024 + content_length = download_resp.headers.get("content-length") + if content_length: + try: + if int(content_length) > max_size: + raise ValueError( + f"文件 Content-Length {content_length} bytes 超过平台限制 {max_size} bytes (500KB)" + ) + except ValueError as e: + if "超过平台限制" in str(e): + raise + # 非数字时忽略 + + file_content = download_resp.content + + # 2. 文件大小校验(平台限制 500KB) + if len(file_content) > max_size: + raise ValueError( + f"文件大小 {len(file_content)} bytes 超过平台限制 {max_size} bytes (500KB)," + "请先压缩或拆分后重试" + ) + + # 3. 解析文件名并校验扩展名 + file_name = self._extract_file_name(download_resp, file_url) + allowed_exts = (".xlsx", ".xls") + if not file_name.lower().endswith(allowed_exts): + raise ValueError( + f"不支持的文件类型: {file_name},仅支持 {', '.join(allowed_exts)}" + ) + + # 4. 推断 MIME 类型 + content_type, _ = mimetypes.guess_type(file_name) + if not content_type: + content_type = ( + "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + if file_name.lower().endswith(".xlsx") + else "application/vnd.ms-excel" + ) + + # 5. 构建文件上传 files = { - "file": (file_name, io.BytesIO(file_content), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"), + "file": (file_name, io.BytesIO(file_content), content_type), } - + return await self.client.upload( f"/datasource/connection/{connection_id}/import_document/preview", files=files, params={"target": target}, ) + @staticmethod + def _extract_file_name(response, file_url: str) -> str: + """从响应头或 URL 中提取文件名。""" + # 优先从 Content-Disposition 解析 + content_disposition = response.headers.get("content-disposition") + if content_disposition: + # 简单解析 filename="xxx" 或 filename*=UTF-8''xxx + # 注意:filename* 也包含 filename= 前缀,必须先检查 filename* + for part in content_disposition.split(";"): + part = part.strip() + if part.lower().startswith("filename*="): + encoded = part.split("=", 1)[1] + # 形如 UTF-8''xxx + if "''" in encoded: + _, _, file_name = encoded.partition("''") + return os.path.basename(unquote(file_name, encoding="utf-8")) + return os.path.basename(unquote(encoded, encoding="utf-8")) + if part.lower().startswith("filename="): + file_name = part.split("=", 1)[1].strip('"') + return os.path.basename(unquote(file_name)) + + # fallback 从 URL 路径获取 + parsed = urlparse(file_url) + path = unquote(parsed.path) + file_name = os.path.basename(path) if path else "" + if file_name and "." in file_name: + return file_name + + return "import.xlsx" + @register_tool("confirm_import_data") class ConfirmImportDataTool(ToolDef): name = "confirm_import_data" - description = "确认导入 AI 识别后的数据" + description = ( + "确认导入 AI 识别后的数据(建表+插数据)。" + "传入 preview_import_data 返回的 data 原文 + databaseName 即可," + "工具内部会自动组装成后端要求的 {tableStructure(含databaseName), allData} 结构" + ) 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"}, + "databaseName": {"type": "string", "description": "落库的数据库名(导入目标库)"}, + "target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"}, + "data": {"type": "object", "description": "preview_import_data 返回的 data(含 tableStructure/allData),或已组装好的最终结构"}, }, "required": ["connectionId", "data"], } + @staticmethod + def _build_body(data: dict, database_name) -> dict: + """把 preview 返回的 data 组装成 confirm 要求的 body。 + + 后端真实结构(已真机探测确认): + { "tableStructure": <单表对象,含 columns 且需带 databaseName>, "allData": [...] } + preview 返回的 data.tableStructure 是 {success,message,data:{tables:[...]}} 的包装, + 需取出 data.tables[0] 作为单表对象。 + """ + if not isinstance(data, dict): + return data + + # 若传入的是 preview 工具的完整返回信封 {code, msg, data:{...}},先剥一层 + if "tableStructure" not in data and isinstance(data.get("data"), dict): + inner_env = data["data"] + if "tableStructure" in inner_env or "allData" in inner_env: + data = inner_env + + ts = data.get("tableStructure") + single_table = None + if isinstance(ts, dict): + if "columns" in ts: + # 已是单表对象(调用方自行组装过) + single_table = dict(ts) + else: + # preview 包装:tableStructure.data.tables[0] + inner = ts.get("data") if isinstance(ts.get("data"), dict) else {} + tables = inner.get("tables") if isinstance(inner, dict) else None + if isinstance(tables, list) and tables: + single_table = dict(tables[0]) + + if single_table is None: + # 无法识别结构,原样透传(兼容调用方已构造好完整 body 的情况) + return data + + if database_name and not single_table.get("databaseName"): + single_table["databaseName"] = database_name + + all_data = data.get("allData") + if all_data is None: + all_data = data.get("data") or [] + return {"tableStructure": single_table, "allData": all_data} + async def execute(self, args: dict) -> dict: args = dict(args) connection_id = args.pop("connectionId") target = args.pop("target", "test") + database_name = args.pop("databaseName", None) data = args.pop("data") - + + body = self._build_body(data, database_name) return await self.client.post( f"/datasource/connection/{connection_id}/import_document/confirm", - json_data=data, + json_data=body, params={"target": target}, ) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py index e524b44..bb96651 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/database_tables.py @@ -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) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py index c38ea17..134668c 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/datasources.py @@ -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,10 +100,8 @@ 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"], @@ -168,6 +168,82 @@ class DeleteDatasourceTool(ToolDef): except Exception as e: # 记录日志但继续删除 logger.debug(f"停用数据源失败(可能已停用): {e}") - + # 删除数据源 return await self.client.delete(f"/datasource/connection/{ds_id}") + + +@register_tool("test_connection") +class TestConnectionTool(ToolDef): + name = "test_connection" + description = "测试外部数据库连接是否可用(不创建数据源)" + input_schema = { + "type": "object", + "properties": { + "datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"}, + "host": {"type": "string", "description": "数据库地址"}, + "port": {"type": "integer", "description": "端口号"}, + "databaseName": {"type": "string", "description": "要连接的数据库名"}, + "username": {"type": "string", "description": "数据库用户名"}, + "password": {"type": "string", "description": "密码"}, + "connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型,默认 user_password"}, + }, + "required": ["datasourceType", "host", "port", "databaseName", "username"], + } + + async def execute(self, args: dict) -> dict: + args = dict(args) + args.setdefault("connectionType", "user_password") + return await self.client.post("/datasource/connection/test", json_data=args) + + +@register_tool("get_realtime_structure") +class GetRealtimeStructureTool(ToolDef): + name = "get_realtime_structure" + description = "拉取数据源真实的库表结构(实时探测远端数据库)" + input_schema = { + "type": "object", + "properties": { + "datasourceId": {"type": "string", "description": "数据源 ID"}, + }, + "required": ["datasourceId"], + } + + async def execute(self, args: dict) -> dict: + ds_id = args["datasourceId"] + return await self.client.get(f"/datasource/connection/realtime/structure/{ds_id}") + + +@register_tool("create_builtin_datasource") +class CreateBuiltinDatasourceTool(ToolDef): + name = "create_builtin_datasource" + description = "创建内置 PostgreSQL 数据源连接,返回 connectionId(内置源建库建表链路第一步)" + input_schema = { + "type": "object", + "properties": { + "datasourceName": {"type": "string", "description": "数据源名称(3-20字)"}, + "remark": {"type": "string", "description": "数据源描述"}, + }, + "required": ["datasourceName"], + } + + async def execute(self, args: dict) -> dict: + return await self.client.post("/datasource/connection/create_builtin_postgresql", json_data=args) + + +@register_tool("update_builtin_datasource") +class UpdateBuiltinDatasourceTool(ToolDef): + name = "update_builtin_datasource" + description = "修改内置 PostgreSQL 数据源连接信息" + input_schema = { + "type": "object", + "properties": { + "id": {"type": "string", "description": "数据源 ID"}, + "datasourceName": {"type": "string", "description": "更新名称"}, + "remark": {"type": "string", "description": "更新描述"}, + }, + "required": ["id"], + } + + async def execute(self, args: dict) -> dict: + return await self.client.put("/datasource/connection/update_builtin_database", json_data=args) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/mqtt_sync.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/mqtt_sync.py new file mode 100644 index 0000000..4786bb1 --- /dev/null +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/mqtt_sync.py @@ -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") diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py index 1a3af12..590d7eb 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/skills.py @@ -119,14 +119,17 @@ class DeleteSkillToolTool(ToolDef): @register_tool("update_skill_config") class UpdateSkillConfigTool(ToolDef): name = "update_skill_config" - description = "更新技能配置(如 MCP Server 配置模板)" + description = "更新技能(名称/描述/MCP 配置模板等)。对应后端 skill/updateOrGet" input_schema = { "type": "object", "properties": { "datasourceId": {"type": "string", "description": "数据源 ID"}, - "configTemplate": {"type": "string", "description": "配置模板 JSON 字符串"}, + "skillId": {"type": "string", "description": "技能 ID(可选)"}, + "name": {"type": "string", "description": "技能名称(可选)"}, + "description": {"type": "string", "description": "技能描述(可选)"}, + "configTemplate": {"type": "string", "description": "配置模板 JSON 字符串(可选)"}, }, - "required": ["datasourceId", "configTemplate"], + "required": ["datasourceId"], } async def execute(self, args: dict) -> dict: @@ -135,3 +138,40 @@ class UpdateSkillConfigTool(ToolDef): if "configTemplate" in args and isinstance(args["configTemplate"], dict): args["configTemplate"] = json.dumps(args["configTemplate"]) return await self.client.post("/datasource/skill/updateOrGet", json_data=args) + + +@register_tool("update_skill_tool") +class UpdateSkillToolTool(ToolDef): + name = "update_skill_tool" + description = "修改技能下某个工具的名称/描述/SQL等(对应后端 tskilltool/updateOrGet,upsert 语义)" + # 真实工具实体字段(已真机探测确认):主键 id、展示名 uniqueName、描述 description、 + # SQL 模板 sqlTemplate、结果类型 resultType。后端不认 skillToolId/businessDescription/businessScenario。 + input_schema = { + "type": "object", + "properties": { + "id": {"type": "string", "description": "技能工具 ID(get_skill_tools 返回的 id)"}, + "uniqueName": {"type": "string", "description": "工具展示名(可选)"}, + "description": {"type": "string", "description": "工具描述(可选)"}, + "sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"}, + "resultType": {"type": "string", "enum": ["single", "list"], "description": "结果类型(可选)"}, + }, + "required": ["id"], + } + + # 旧参数名 -> 真实字段名(向后兼容此前错误实现的调用方) + _LEGACY_MAP = { + "skillToolId": "id", + "name": "uniqueName", + "businessDescription": "description", + } + + async def execute(self, args: dict) -> dict: + args = dict(args) + for old, new in self._LEGACY_MAP.items(): + if old in args and new not in args: + args[new] = args.pop(old) + else: + args.pop(old, None) + # businessScenario 后端实体无此字段,丢弃避免干扰 + args.pop("businessScenario", None) + return await self.client.post("/datasource/skill/tskilltool/updateOrGet", json_data=args) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/sql_execution.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/sql_execution.py index 07ddf92..4f53352 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/sql_execution.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/sql_execution.py @@ -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, + ) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/subscriptions.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/subscriptions.py index 6080cf0..11799bc 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/subscriptions.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/subscriptions.py @@ -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) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py index 67bf8c0..2d5889c 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/table_data.py @@ -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) diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/__init__.cpython-312.pyc index af8ee54..94343c0 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/__init__.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/api_client.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/api_client.cpython-312.pyc index 995bf01..e9fcfa9 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/api_client.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/api_client.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/env_config.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/env_config.cpython-312.pyc index 0a4b061..8b1c8c7 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/env_config.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/env_config.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/logger_config.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/logger_config.cpython-312.pyc index 6c581fd..dc2d078 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/logger_config.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/logger_config.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py index c0dd25a..19e8ed7 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/api_client.py @@ -64,37 +64,49 @@ class AgileDBAPIClient: 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 响应""" logger.info(f"[API响应] HTTP {response.status_code}") - + if response.status_code == 204: return {"success": True, "data": None} - - response.raise_for_status() - + + # 先尝试解析 body,再判断状态码。 + # 平台会用非 2xx 状态码 + body 里的 {code, msg} 返回业务错误, + # 若先 raise_for_status() 会在解析 body 前抛异常,导致真正的 msg 全部丢失。 try: data = response.json() - except json.JSONDecodeError: - # 非 JSON 响应(如文件下载) + except (json.JSONDecodeError, UnicodeDecodeError): + # 非 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]: @@ -147,9 +159,13 @@ class AgileDBAPIClient: try: logger.info(f"[API请求] DELETE {url}") headers = self._get_headers() + # 注意:httpx 的 client.delete() 便捷方法不接受 json 参数(仅 post/put/patch 有)。 + # 需要带 body 的 DELETE 必须走通用 request(),否则会抛 TypeError。 if json_data is not None: headers['Content-Type'] = 'application/json' - response = await self.client.delete(url, headers=headers, params=params, json=json_data) + response = await self.client.request("DELETE", url, headers=headers, params=params, json=json_data) + else: + response = await self.client.delete(url, headers=headers, params=params) return self._handle_response(response, url) except httpx.TimeoutException: raise Exception(f"API 请求超时: {url}") diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py index 6e19b1d..87364db 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/env_config.py @@ -44,7 +44,7 @@ def get_env_config() -> dict: dict: 包含所有配置的字典 """ return { - "api_key": get_api_key(""), + "api_key": os.environ.get("API_KEY", ""), "base_url": get_base_url(), } diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库模块-功能与业务链路说明.md b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库模块-功能与业务链路说明.md new file mode 100644 index 0000000..d26e609 --- /dev/null +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库模块-功能与业务链路说明.md @@ -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 | diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-MCP工具设计方案.md b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-MCP工具设计方案.md index 33cec1a..70deb5e 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-MCP工具设计方案.md +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-MCP工具设计方案.md @@ -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,18 @@ --- +#### 23.5 `revoke_api_key_permissions` +- **用途**:撤销/删除 API 密钥已授予的权限(按权限记录 ID) +- **对应 API**:按现有 delete 风格推测为 `DELETE /api/datasource/api_key/permission/{ids}`,需后端真机验证 + +| 参数 | 类型 | 必填 | 说明 | +|------|------|------|------| +| permissionIds | array[string] | 是 | 权限记录 ID 列表。先从 `get_api_key_permissions` 获取,取 `connectionPermissions` / `databasePermissions` / `tablePermissions` 中每项的 `id` | + +**返回**:撤销结果 + +--- + ### 🤖 技能与工具管理 (内置数据源 AI 能力) #### 24. `get_skill_by_datasource` @@ -437,12 +445,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 | 否 | 业务场景描述 | **返回**:工具创建结果 diff --git a/lzwcai_mcp_agile_db/main.py b/lzwcai_mcp_agile_db/main.py index 4fea9dd..d86d2da 100644 --- a/lzwcai_mcp_agile_db/main.py +++ b/lzwcai_mcp_agile_db/main.py @@ -7,6 +7,6 @@ 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" + os.environ["API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6ImNlMDAwYjA4LWU0YTYtNGM2MS1hNzJiLWI3NTlmNmY1N2Q4NCJ9.jiNmGQZfL4-nSIFrLuaCt7mT5zj0FOojAVkLeHwPOroI5jBxodrCe1PSwGO1OHq5Ztb0tLEVZw2FFVj0OlTceQ" + os.environ["backendBaseUrl"] = "http://192.168.2.236:8088" main() diff --git a/lzwcai_mcp_agile_db/pyproject.toml b/lzwcai_mcp_agile_db/pyproject.toml index a740844..8509d38 100644 --- a/lzwcai_mcp_agile_db/pyproject.toml +++ b/lzwcai_mcp_agile_db/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "lzwcai-mcp-agile-db" -version = "0.1.3" +version = "0.1.7" description = "MCP server for database management platform with 33 tools for datasource, table, data, API key, and skill management" readme = "README.md" requires-python = ">=3.10" diff --git a/lzwcai_mcp_agile_db_third/.python-version b/lzwcai_mcp_agile_db_third/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/lzwcai_mcp_agile_db_third/README.md b/lzwcai_mcp_agile_db_third/README.md new file mode 100644 index 0000000..b3db6d5 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/README.md @@ -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` 等敏感字段进行脱敏 diff --git a/lzwcai_mcp_agile_db_third/__pycache__/main.cpython-312.pyc b/lzwcai_mcp_agile_db_third/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000..e370a66 Binary files /dev/null and b/lzwcai_mcp_agile_db_third/__pycache__/main.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/.gitignore b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/.gitignore new file mode 100644 index 0000000..3e2c027 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/.gitignore @@ -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 diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/API_DOCUMENTATION.md b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/API_DOCUMENTATION.md new file mode 100644 index 0000000..2e031fb --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/API_DOCUMENTATION.md @@ -0,0 +1,1620 @@ +# 数据源管理接口文档 + +## 目录 +- [1. 数据源配置管理 (DatasourceConfigController)](#1-数据源配置管理) +- [2. 数据库连接实例管理 (DatasourceConnectionController)](#2-数据库连接实例管理) + +--- + +## 1. 数据源配置管理 + +**Base URL**: `/datasource/config` + +### 1.1 查询数据源配置列表 + +**接口**: `GET /datasource/config/list` + +**描述**: 查询数据源配置列表,支持分页和多条件筛选 + +**请求参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| datasourceName | String | 否 | 数据源名称(模糊查询) | +| datasourceId | Long | 否 | 连接ID | +| datasourceType | String | 否 | 数据库类型 | +| status | Integer | 否 | 状态 | +| showTable | Boolean | 否 | 是否显示表数量 | +| pageNum | Integer | 否 | 页码(默认1) | +| pageSize | Integer | 否 | 每页数量(默认10) | + +**响应示例**: +```json +{ + "code": 200, + "msg": "查询成功", + "total": 10, + "rows": [ + { + "id": 1, + "datasourceName": "测试数据库", + "connectionId": 1, + "databaseName": "test_db", + "datasourceType": "PostgreSQL", + "status": 0, + "skillBool": true, + "tableNum": 5, + "createTime": "2025-01-01 10:00:00" + } + ] +} +``` + +### 1.2 获取数据源配置详情 + +**接口**: `GET /datasource/config/{id}` + +**描述**: 获取指定数据源配置的详细信息 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 数据源配置ID | + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "id": 1, + "datasourceName": "测试数据库", + "connectionId": 1, + "databaseName": "test_db", + "datasourceType": "PostgreSQL", + "status": 0, + "skillBool": true, + "tableNum": 5 + } +} +``` + +### 1.3 批量新增数据源配置 + +**接口**: `POST /datasource/config` + +**描述**: 批量创建数据源配置并同步表结构 + +**请求体**: +```json +{ + "connectionId": 1, + "datasourceNamePrefix": "业务", + "enterpriseId": 1, + "status": 0, + "remark": "备注信息", + "syncTables": true, + "databases": [ + { + "databaseName": "db1", + "tableNames": ["table1", "table2"] + }, + { + "databaseName": "db2", + "tableNames": ["table3"] + } + ] +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "成功创建2个数据源配置,正在后台异步同步3张表,请稍后刷新查看", + "data": { + "successCount": 2, + "totalSyncTableCount": 3, + "results": [ + { + "databaseName": "db1", + "datasourceConfig": { "id": 1, "datasourceName": "业务-db1" }, + "syncTableCount": 2 + } + ] + } +} +``` + +### 1.4 全量替换数据源配置 + +**接口**: `PUT /datasource/config` + +**描述**: 全量替换指定连接下的数据源配置(未传入的配置将被删除) + +**⚠️ 警告**: 会删除未传入的所有数据库配置及其相关表元数据,请谨慎操作! + +**请求体**: 同1.3 + +**响应示例**: +```json +{ + "code": 200, + "msg": "全量替换成功:创建1个,更新0个,删除1个,正在后台异步同步2张表", + "data": { + "createdCount": 1, + "updatedCount": 0, + "deletedCount": 1, + "totalSyncTableCount": 2, + "results": [...] + } +} +``` + +### 1.5 批量修改数据源配置 + +**接口**: `PUT /datasource/config/batch` + +**描述**: 批量修改数据源配置并重新同步表结构 + +**请求体**: +```json +{ + "status": 0, + "remark": "批量更新", + "syncTables": true, + "datasources": [ + { + "id": 1, + "datasourceName": "新名称", + "tableNames": ["table1", "table2"] + } + ] +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "成功修改1个数据源配置,正在后台异步同步2张表", + "data": { + "successCount": 1, + "totalSyncTableCount": 2, + "results": [...] + } +} +``` + +### 1.6 删除数据源配置 + +**接口**: `POST /datasource/config/deletes` + +**描述**: 批量删除数据源配置 + +**请求体**: +```json +{ + "ids": [1, 2, 3] +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功" +} +``` + +### 1.7 测试数据库连接 + +**接口**: `POST /datasource/config/testConnectionConfig` + +**描述**: 测试数据库连接是否正常 + +**请求体**: +```json +{ + "host": "localhost", + "port": 5432, + "username": "postgres", + "password": "password", + "datasourceType": "PostgreSQL" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "连接成功" +} +``` + +### 1.8 修改数据源状态 + +**接口**: `PUT /datasource/config/changeStatus` + +**描述**: 修改数据源配置的启用/禁用状态 + +**请求体**: +```json +{ + "id": 1, + "status": 1 +} +``` + +### 1.9 导出数据源配置 + +**接口**: `POST /datasource/config/export` + +**描述**: 导出数据源配置列表为Excel文件 + +--- + +## 2. 数据库连接实例管理 + +**Base URL**: `/datasource/connection` + +### 2.1 查询连接实例列表 + +**接口**: `GET /datasource/connection/list` + +**描述**: 查询数据库连接实例列表,支持分页和筛选(已优化N+1查询问题) + +**请求参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| datasourceName | String | 否 | 数据源名称(模糊查询) | +| status | Integer | 否 | 状态 | +| testStatus | Integer | 否 | 测试状态(0未测试/1成功/2失败) | +| sourceType | String | 否 | 连接来源(builtin/external) | +| pageNum | Integer | 否 | 页码 | +| pageSize | Integer | 否 | 每页数量 | + +**响应示例**: +```json +{ + "code": 200, + "msg": "查询成功", + "total": 5, + "rows": [ + { + "id": 1, + "datasourceName": "内置PostgreSQL", + "datasourceType": "PostgreSQL", + "host": "localhost", + "port": 5432, + "status": 0, + "testStatus": 1, + "sourceType": "builtin", + "databaseNum": 3, + "tableNum": 15, + "datasourceConfig": [...] + } + ] +} +``` + +### 2.2 获取连接实例详情 + +**接口**: `GET /datasource/connection/{id}` + +**描述**: 获取指定连接实例的详细信息 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 连接实例ID | + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "id": 1, + "datasourceName": "内置PostgreSQL", + "host": "localhost", + "port": 5432, + "username": "postgres", + "databaseNum": 3, + "tableNum": 15, + "datasourceConfig": [ + { + "id": 1, + "databaseName": "db1", + "tables": [ + {"tableName": "table1", "tableId": 1} + ], + "skillBool": true + } + ] + } +} +``` + +### 2.3 新增连接实例 + +**接口**: `POST /datasource/connection` + +**描述**: 创建新的数据库连接实例 + +**请求体**: +```json +{ + "datasourceName": "测试连接", + "datasourceType": "PostgreSQL", + "connectionType": "user_password", + "host": "localhost", + "port": 5432, + "username": "postgres", + "password": "password", + "status": 0, + "remark": "备注信息" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "创建成功", + "data": { + "id": 1, + "datasourceName": "测试连接" + } +} +``` + +### 2.4 修改连接实例 + +**接口**: `PUT /datasource/connection` + +**描述**: 修改数据库连接实例信息 + +**请求体**: +```json +{ + "id": 1, + "datasourceName": "新名称", + "host": "localhost", + "port": 5432, + "username": "postgres", + "password": "new_password", + "remark": "更新备注" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "更新成功" +} +``` + +### 2.5 删除连接实例 + +**接口**: `DELETE /datasource/connection/{id}` + +**描述**: 删除指定的连接实例 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 连接实例ID | + +**响应示例**: +```json +{ + "code": 200, + "msg": "删除数据库实例成功" +} +``` + +### 2.6 测试连接 + +**接口**: `POST /datasource/connection/test` + +**描述**: 测试数据库连接是否可用 + +**请求体**: +```json +{ + "id": 1, + "host": "localhost", + "port": 5432, + "username": "postgres", + "password": "password", + "datasourceType": "PostgreSQL" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "连接成功" +} +``` + +### 2.7 修改连接状态 + +**接口**: `PUT /datasource/connection/changeStatus` + +**描述**: 修改连接实例的启用/禁用状态 + +**请求体**: +```json +{ + "id": 1, + "status": 1 +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "状态修改成功" +} +``` + +### 2.8 实时查询数据库结构 + +**接口**: `GET /datasource/connection/realtime/structure/{id}` + +**描述**: 实时查询连接下的所有数据库和表结构(直接连接数据库服务器查询) + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 连接实例ID | + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": [ + { + "databaseName": "db1", + "tables": ["table1", "table2"], + "tableCount": 2 + } + ] +} +``` + +### 2.9 实时查询数据库列表 + +**接口**: `GET /datasource/connection/realtime/databases/{id}` + +**描述**: 实时查询连接下的所有数据库名称 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 连接实例ID | + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": ["db1", "db2", "db3"] +} +``` + +### 2.10 实时查询表列表 + +**接口**: `GET /datasource/connection/realtime/tables/{id}` + +**描述**: 实时查询指定数据库下的所有表 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| id | Long | 是 | 连接实例ID | + +**查询参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| databaseName | String | 是 | 数据库名称 | + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": ["table1", "table2", "table3"] +} +``` + +### 2.11 创建内置PostgreSQL连接 + +**接口**: `POST /datasource/connection/create_builtin_postgresql` + +**描述**: 创建内置PostgreSQL数据库连接(使用配置文件中的连接信息) + +**请求体**: +```json +{ + "datasourceName": "内置PostgreSQL数据库", + "remark": "系统内置数据库" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "✅测试通过,连接通过", + "data": { + "id": 1, + "datasourceName": "内置PostgreSQL数据库" + } +} +``` + +### 2.12 修改内置数据库连接 + +**接口**: `PUT /datasource/connection/update_builtin_database` + +**描述**: 修改内置PostgreSQL连接信息 + +**请求体**: +```json +{ + "connectionId": 1, + "datasourceName": "新名称", + "remark": "更新备注" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "✅保存成功" +} +``` + +### 2.13 执行SQL语句 + +**接口**: `POST /datasource/connection/{datasourceId}/execute_sql` + +**描述**: 在指定数据源上执行SQL语句,支持参数化查询和环境切换 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| datasourceId | Long | 是 | 数据源配置ID | + +**请求头**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| X-Datasource-API-Key | String | 否 | API密钥(用于权限验证) | + +**查询参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| target | String | 否 | 目标环境(prod/test,默认prod) | + +**请求体**: +```json +{ + "sql": "SELECT * FROM users WHERE age > ?", + "params": [18], + "databaseName": "test_db" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "success": true, + "affectedRows": 10, + "duration": 25, + "target": "prod", + "databaseName": "test_db", + "msg": "执行成功,此次操作影响10行数据", + "rows": [ + {"id": 1, "name": "张三", "age": 25} + ], + "affectedRowsDetail": { + "operationType": "SELECT", + "tables": ["users"], + "count": 10 + } + } +} +``` + +**说明**: +- 支持参数化查询,使用 `?` 占位符 +- 自动识别SQL类型(SELECT/INSERT/UPDATE/DELETE等) +- 支持DDL操作(CREATE/ALTER/DROP) +- 内置连接支持自动同步到测试库 + +### 2.14 创建数据库 + +**接口**: `POST /datasource/connection/{connectionId}/create_database` + +**描述**: 在指定连接上创建新数据库(目前仅支持PostgreSQL) + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| connectionId | Long | 是 | 连接实例ID | + +**请求头**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| X-Datasource-API-Key | String | 否 | API密钥 | + +**请求体**: +```json +{ + "databaseName": "new_database", + "encoding": "UTF8", + "owner": "postgres" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "创建数据库成功" +} +``` + +**说明**: +- 需要连接级别的 CREATE_DATABASE 权限 +- 内置连接会自动创建对应的测试库(_test后缀) +- 创建成功后会自动创建数据源配置 + +### 2.15 创建表 + +**接口**: `POST /datasource/connection/{connectionId}/create_table` + +**描述**: 在指定数据库中创建新表 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| connectionId | Long | 是 | 连接实例ID | + +**请求头**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| X-Datasource-API-Key | String | 否 | API密钥 | + +**请求体**: +```json +{ + "databaseName": "test_db", + "tableName": "users", + "tableComment": "用户表", + "columns": [ + { + "columnName": "id", + "columnType": "BIGINT", + "isPrimaryKey": true, + "isNullable": false, + "columnComment": "主键ID" + }, + { + "columnName": "name", + "columnType": "VARCHAR", + "columnLength": 100, + "isNullable": false, + "columnComment": "用户名" + }, + { + "columnName": "age", + "columnType": "INTEGER", + "isNullable": true, + "columnComment": "年龄" + } + ] +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "创建表成功" +} +``` + +**说明**: +- 需要数据库级别的 CREATE 权限 +- 内置连接会自动在测试库中创建相同表结构 +- 创建成功后会自动同步表元数据 + +### 2.16 创建数据库和表 + +**接口**: `POST /datasource/connection/{connectionId}/create_database_table` + +**描述**: 同时创建数据库和表(一次性操作) + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| connectionId | Long | 是 | 连接实例ID | + +**请求头**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| X-Datasource-API-Key | String | 否 | API密钥 | + +**请求体**: +```json +{ + "databaseName": "new_db", + "encoding": "UTF8", + "owner": "postgres", + "tables": [ + { + "tableName": "users", + "tableComment": "用户表", + "columns": [ + { + "columnName": "id", + "columnType": "BIGINT", + "isPrimaryKey": true, + "isNullable": false, + "columnComment": "主键ID" + } + ] + }, + { + "tableName": "orders", + "tableComment": "订单表", + "columns": [...] + } + ] +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "创建成功" +} +``` + +**说明**: +- 需要连接级别的 CREATE_DATABASE 权限和数据库级别的 CREATE 权限 +- 支持一次创建多个表 +- 内置连接会自动创建测试库和测试表 + +### 2.17 修改数据库 + +**接口**: `PUT /datasource/connection/{connectionId}/alter_database` + +**描述**: 修改数据库属性(重命名、更改所有者、更改编码) + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| connectionId | Long | 是 | 连接实例ID | + +**请求头**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| X-Datasource-API-Key | String | 否 | API密钥 | + +**请求体**: +```json +{ + "databaseName": "old_db", + "newName": "new_db", + "newOwner": "postgres", + "newEncoding": "UTF8" +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "修改数据库成功" +} +``` + +**说明**: +- 需要数据库级别的 ALTER 权限 +- 至少需要指定一个修改项(newName/newOwner/newEncoding) +- 内置连接会同步修改测试库 + +### 2.18 修改表 + +**接口**: `PUT /datasource/connection/{connectionId}/alter_table` + +**描述**: 修改表结构(添加列、删除列、重命名列、修改列类型等) + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| connectionId | Long | 是 | 连接实例ID | + +**请求头**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| X-Datasource-API-Key | String | 否 | API密钥 | + +**请求体**: +```json +{ + "databaseName": "test_db", + "tableName": "users", + "tableComment": "更新的表注释", + "operations": [ + { + "operation": "ADD_COLUMN", + "column": { + "columnName": "email", + "columnType": "VARCHAR", + "columnLength": 255, + "isNullable": true, + "columnComment": "邮箱" + } + }, + { + "operation": "DROP_COLUMN", + "column": { + "columnName": "old_field" + } + }, + { + "operation": "RENAME_COLUMN", + "column": { + "columnName": "old_name", + "newColumnName": "new_name" + } + }, + { + "operation": "ALTER_COLUMN_TYPE", + "column": { + "columnName": "age", + "columnType": "BIGINT" + } + } + ] +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "修改表成功" +} +``` + +**支持的操作类型**: +- `ADD_COLUMN`: 添加列 +- `DROP_COLUMN`: 删除列 +- `RENAME_COLUMN`: 重命名列 +- `ALTER_COLUMN_TYPE`: 修改列类型 +- `SET_NOT_NULL`: 设置列为非空 +- `DROP_NOT_NULL`: 设置列为可空 +- `SET_DEFAULT`: 设置默认值 +- `DROP_DEFAULT`: 删除默认值 + +**说明**: +- 需要表级别的 ALTER 权限 +- 支持批量操作 +- 内置连接会同步修改测试表 +- 如果列有 columnId,系统会自动检测并插入重命名操作 + +### 2.19 生成表结构 + +**接口**: `POST /datasource/connection/generate_table` + +**描述**: 使用AI根据需求描述生成表结构(异步任务) + +**请求体**: +```json +{ + "requirement": "创建一个用户管理系统,需要用户表、角色表和权限表,用户表包含用户名、密码、邮箱、手机号等字段", + "databaseId": 1 +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "taskId": 123, + "status": "PENDING", + "message": "任务已提交,等待AI生成表结构" + } +} +``` + +**说明**: +- 异步任务,需要轮询任务状态 +- AI会根据需求描述自动生成表结构 +- 可以指定关联的数据库ID + +### 2.20 文档导入预览 + +**接口**: `POST /datasource/connection/{connectionId}/import_document/preview` + +**描述**: 上传Excel/CSV文件,AI识别表结构并预览前10条数据 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| connectionId | Long | 是 | 连接实例ID | + +**查询参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| target | String | 否 | 目标环境(prod/test,默认prod) | + +**请求参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| file | MultipartFile | 是 | Excel或CSV文件 | + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "tableStructure": { + "databaseName": "test_db", + "tableName": "users", + "tableComment": "用户表", + "columns": [ + { + "columnName": "name", + "columnType": "VARCHAR", + "columnLength": 100, + "columnComment": "姓名" + } + ] + }, + "previewData": [ + ["name", "age", "gender"], + ["张三", "25", "male"], + ["李四", "30", "female"] + ], + "allData": [...], + "fileName": "users.xlsx", + "target": "prod" + } +} +``` + +**说明**: +- 支持 Excel (.xlsx, .xls) 和 CSV 文件 +- AI自动识别列名、类型和注释 +- 返回前10条数据用于预览 +- 完整数据保存在 allData 中,用于确认导入 + +### 2.21 文档导入确认 + +**接口**: `POST /datasource/connection/{connectionId}/import_document/confirm` + +**描述**: 确认导入,创建表并插入数据 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| connectionId | Long | 是 | 连接实例ID | + +**请求头**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| X-Datasource-API-Key | String | 否 | API密钥 | + +**查询参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| target | String | 否 | 目标环境(prod/test,默认prod) | + +**请求体**: +```json +{ + "tableStructure": { + "databaseName": "test_db", + "tableName": "users", + "tableComment": "用户表", + "columns": [ + { + "columnName": "name", + "columnType": "VARCHAR", + "columnLength": 100, + "columnComment": "姓名" + } + ] + }, + "allData": [ + ["name", "age", "gender"], + ["张三", "25", "male"], + ["李四", "30", "female"] + ] +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "文档导入成功", + "data": { + "tableName": "users", + "databaseName": "test_db", + "target": "prod", + "insertedRows": 100 + } +} +``` + +**说明**: +- 需要数据库级别的 CREATE 权限 +- 如果表已存在,只插入数据 +- 支持批量插入(每批1000条) +- 自动类型转换和数据验证 +- 内置连接会同步到测试库 + +### 2.22 查询表结构和数据 + +**接口**: `GET /datasource/connection/builtin/table/{tableId}` + +**描述**: 根据表ID查询表结构和数据(分页) + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| tableId | Long | 是 | 表ID | + +**查询参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| pageNum | Integer | 否 | 页码(默认1) | +| pageSize | Integer | 否 | 每页数量(默认100) | +| target | String | 否 | 目标环境(prod/test,默认prod) | + +**响应示例**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": { + "tables": { + "tableName": "users", + "tableComment": "用户表", + "columns": [ + { + "columnName": "id", + "columnType": "BIGINT", + "columnComment": "主键", + "columnLength": null, + "isPrimaryKey": true, + "isNullable": false + }, + { + "columnName": "name", + "columnType": "VARCHAR", + "columnComment": "姓名", + "columnLength": 100, + "isPrimaryKey": false, + "isNullable": false + } + ] + }, + "content": [ + [1, "张三", 25], + [2, "李四", 30] + ], + "pageNum": 1, + "pageSize": 100, + "total": 2, + "target": "prod", + "databaseName": "test_db" + } +} +``` + +**说明**: +- 返回表结构和数据(二维数组格式) +- 列按 ordinalPosition 排序 +- 支持分页查询 +- 可切换正式库和测试库 + +### 2.23 新增表数据 + +**接口**: `POST /datasource/connection/builtin/table/{tableId}/rows` + +**描述**: 向指定表插入一条数据 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| tableId | Long | 是 | 表ID | + +**查询参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| target | String | 否 | 目标环境(prod/test,默认prod) | + +**请求体**: +```json +{ + "data": { + "name": "张三", + "age": 25, + "email": "zhangsan@example.com", + "created_at": "2025-01-01 10:00:00" + } +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "插入成功" +} +``` + +**说明**: +- 只能插入表中存在的列 +- 自动进行类型转换 +- 支持环境切换(正式库/测试库) + +### 2.24 更新表数据 + +**接口**: `PUT /datasource/connection/builtin/table/{tableId}/rows` + +**描述**: 根据主键更新表数据 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| tableId | Long | 是 | 表ID | + +**查询参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| target | String | 否 | 目标环境(prod/test,默认prod) | + +**请求体**: +```json +{ + "data": { + "name": "李四", + "age": 30, + "email": "lisi@example.com" + }, + "primaryKey": { + "id": 1 + } +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "更新成功" +} +``` + +**说明**: +- 必须提供完整的主键条件 +- 只更新 data 中指定的字段 +- 支持复合主键 +- 自动进行类型转换 + +### 2.25 删除表数据 + +**接口**: `DELETE /datasource/connection/builtin/table/{tableId}/rows` + +**描述**: 根据主键批量删除表数据 + +**路径参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| tableId | Long | 是 | 表ID | + +**查询参数**: +| 参数名 | 类型 | 必填 | 说明 | +|--------|------|------|------| +| target | String | 否 | 目标环境(prod/test,默认prod) | + +**请求体**: +```json +{ + "primaryKeys": [ + {"id": 1}, + {"id": 2}, + {"id": 3} + ] +} +``` + +**响应示例**: +```json +{ + "code": 200, + "msg": "删除成功" +} +``` + +**说明**: +- 支持批量删除 +- 必须提供完整的主键条件 +- 支持复合主键 +- 自动进行类型转换 + +--- + +## 3. 通用说明 + +### 3.1 权限验证 + +部分接口需要在请求头中携带 `X-Datasource-API-Key` 进行权限验证。 + +**三层权限模型**: +1. **连接级别权限**: CREATE_DATABASE, DROP_DATABASE +2. **数据库级别权限**: CREATE, ALTER, DROP +3. **表级别权限**: SELECT, INSERT, UPDATE, DELETE, ALTER + +**权限验证规则**: +- 创建/删除数据库:需要连接级别权限 +- 创建/修改/删除表:需要数据库级别权限 +- 数据操作(增删改查):需要表级别权限 +- 修改表结构:需要表级别的 ALTER 权限 + +### 3.2 环境切换 + +支持 `target` 参数切换正式环境(prod)和测试环境(test)。 + +**内置连接特性**: +- 自动创建测试库(数据库名_test) +- DDL操作自动同步到测试库 +- 可独立操作测试库数据 + +**使用场景**: +- `target=prod`: 操作正式库(默认) +- `target=test`: 操作测试库(仅内置连接支持) + +### 3.3 响应格式 + +所有接口统一返回格式: + +**成功响应**: +```json +{ + "code": 200, + "msg": "操作成功", + "data": {} +} +``` + +**失败响应**: +```json +{ + "code": 500, + "msg": "错误信息" +} +``` + +**分页响应**: +```json +{ + "code": 200, + "msg": "查询成功", + "total": 100, + "rows": [...] +} +``` + +### 3.4 数据类型支持 + +**PostgreSQL类型映射**: +- 整数类型: SMALLINT, INTEGER, BIGINT, SERIAL, BIGSERIAL +- 小数类型: DECIMAL, NUMERIC, REAL, DOUBLE PRECISION, MONEY +- 布尔类型: BOOLEAN +- 字符类型: VARCHAR, CHAR, TEXT +- 日期时间: DATE, TIME, TIMESTAMP +- JSON类型: JSON, JSONB +- 数组类型: ARRAY +- UUID类型: UUID + +**类型转换规则**: +- 字符串 → 数字:自动转换 +- 字符串 → 布尔:true/false/1/0 +- 字符串 → 日期:ISO格式自动解析 +- null/空字符串:根据列定义处理 + +### 3.5 异步任务 + +部分操作为异步执行: +- 表结构同步(syncTables) +- AI生成表结构(generate_table) + +**异步任务特点**: +- 立即返回任务ID +- 后台执行,不阻塞主线程 +- 需要轮询任务状态或查看日志 + +### 3.6 批量操作 + +支持批量操作的接口: +- 批量创建数据源配置 +- 批量修改数据源配置 +- 批量删除数据源配置 +- 批量删除表数据 +- 批量插入数据(文档导入) + +**批量操作限制**: +- 单次批量插入最多1000条 +- 批量操作失败会返回详细错误信息 + +### 3.7 错误码说明 + +| 错误码 | 说明 | +|--------|------| +| 200 | 操作成功 | +| 500 | 服务器内部错误 | +| 400 | 参数错误 | +| 401 | 未授权(API Key无效) | +| 403 | 权限不足 | +| 404 | 资源不存在 | + +### 3.8 最佳实践 + +**1. 连接管理**: +- 创建连接后先测试连接 +- 使用内置连接时启用测试库功能 +- 定期检查连接状态 + +**2. 数据源配置**: +- 使用有意义的数据源名称前缀 +- 合理使用异步同步功能 +- 定期清理无用配置 + +**3. SQL执行**: +- 使用参数化查询防止SQL注入 +- 大批量操作建议分批执行 +- 先在测试库验证再操作正式库 + +**4. 权限管理**: +- 遵循最小权限原则 +- 定期审计API Key权限 +- 敏感操作使用独立API Key + +**5. 文档导入**: +- 先预览再确认导入 +- 检查AI识别的表结构是否正确 +- 大文件建议分批导入 + +**6. 表结构修改**: +- 重要操作前备份数据 +- 使用测试库验证修改操作 +- 注意列重命名的自动检测机制 + +### 3.9 注意事项 + +1. **内置连接特性**: + - 仅内置PostgreSQL连接支持测试库功能 + - 测试库名称为:原数据库名_test + - DDL操作会自动同步到测试库 + +2. **权限验证**: + - 当前权限验证已临时禁用(代码中 `if (true) return null;`) + - 生产环境需启用权限验证 + +3. **数据类型**: + - 目前仅完整支持PostgreSQL + - 其他数据库类型支持有限 + +4. **异步操作**: + - 表结构同步为异步操作 + - 需要等待同步完成后才能看到表元数据 + +5. **文件上传**: + - 支持Excel (.xlsx, .xls) 和CSV格式 + - 文件大小建议不超过10MB + - 超大文件建议分批导入 + +--- + +## 4. 使用示例 + +### 4.1 完整流程示例:创建数据库并导入数据 + +**步骤1:创建连接实例** +```bash +POST /datasource/connection +{ + "datasourceName": "测试连接", + "datasourceType": "PostgreSQL", + "host": "localhost", + "port": 5432, + "username": "postgres", + "password": "password" +} +``` + +**步骤2:测试连接** +```bash +POST /datasource/connection/test +{ + "id": 1, + "host": "localhost", + "port": 5432, + "username": "postgres", + "password": "password", + "datasourceType": "PostgreSQL" +} +``` + +**步骤3:创建数据库和表** +```bash +POST /datasource/connection/1/create_database_table +{ + "databaseName": "my_app", + "encoding": "UTF8", + "tables": [ + { + "tableName": "users", + "tableComment": "用户表", + "columns": [ + { + "columnName": "id", + "columnType": "BIGINT", + "isPrimaryKey": true, + "isNullable": false, + "columnComment": "用户ID" + }, + { + "columnName": "username", + "columnType": "VARCHAR", + "columnLength": 50, + "isNullable": false, + "columnComment": "用户名" + } + ] + } + ] +} +``` + +**步骤4:查询数据源配置** +```bash +GET /datasource/config/list?datasourceName=my_app +``` + +**步骤5:插入数据** +```bash +POST /datasource/connection/builtin/table/1/rows +{ + "data": { + "id": 1, + "username": "admin" + } +} +``` + +### 4.2 文档导入示例 + +**步骤1:上传文件预览** +```bash +POST /datasource/connection/1/import_document/preview +Content-Type: multipart/form-data + +file: users.xlsx +target: test +``` + +**步骤2:确认导入** +```bash +POST /datasource/connection/1/import_document/confirm?target=test +{ + "tableStructure": { + "databaseName": "my_app", + "tableName": "imported_users", + "columns": [...] + }, + "allData": [...] +} +``` + +### 4.3 SQL执行示例 + +**查询数据** +```bash +POST /datasource/connection/1/execute_sql?target=prod +{ + "sql": "SELECT * FROM users WHERE age > ? AND status = ?", + "params": [18, "active"], + "databaseName": "my_app" +} +``` + +**更新数据** +```bash +POST /datasource/connection/1/execute_sql?target=test +{ + "sql": "UPDATE users SET status = ? WHERE id = ?", + "params": ["inactive", 1], + "databaseName": "my_app" +} +``` + +--- + +## 5. 附录 + +### 5.1 数据库类型枚举 + +| 枚举值 | 显示名称 | 默认端口 | 默认测试库 | +|--------|----------|----------|------------| +| POSTGRESQL | PostgreSQL | 5432 | postgres | +| MYSQL | MySQL | 3306 | mysql | +| ORACLE | Oracle | 1521 | - | +| SQLSERVER | SQL Server | 1433 | master | + +### 5.2 状态码枚举 + +| 状态码 | 说明 | +|--------|------| +| 0 | 正常 | +| 1 | 停用 | + +### 5.3 测试状态枚举 + +| 状态码 | 说明 | +|--------|------| +| 0 | 未测试 | +| 1 | 连接成功 | +| 2 | 连接失败 | + +### 5.4 连接来源枚举 + +| 来源类型 | 说明 | +|----------|------| +| builtin | 内置连接(支持测试库) | +| external | 外部连接 | + +--- + +## 6. 更新日志 + +### v1.0.0 (2025-12-26) +- 初始版本 +- 支持数据源配置管理 +- 支持数据库连接实例管理 +- 支持SQL执行 +- 支持DDL操作(创建/修改数据库和表) +- 支持文档导入(Excel/CSV) +- 支持AI生成表结构 +- 支持内置PostgreSQL测试库功能 +- 支持三层权限模型 +- 优化N+1查询问题 + +--- + +**文档维护**: 技术团队 +**最后更新**: 2025-12-26 +**版本**: v1.0.0 diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/README.md b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/README.md new file mode 100644 index 0000000..b3db6d5 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/README.md @@ -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` 等敏感字段进行脱敏 diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/__init__.py b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/__init__.py new file mode 100644 index 0000000..96053c9 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/__init__.py @@ -0,0 +1 @@ +"""lzwcai_mcp_agile_db_third package.""" diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/main.py b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/main.py new file mode 100644 index 0000000..c31b1e0 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/main.py @@ -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() diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/mock_db.py b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/mock_db.py new file mode 100644 index 0000000..474132d --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/mock_db.py @@ -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() diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/pyproject.toml b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/pyproject.toml new file mode 100644 index 0000000..db1688a --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/pyproject.toml @@ -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/**", +] diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/tools.py b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/tools.py new file mode 100644 index 0000000..5b6c464 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/tools.py @@ -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) diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/__init__.py b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/__init__.py new file mode 100644 index 0000000..5643e8a --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/__init__.py @@ -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", +] diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/api_client.py b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/api_client.py new file mode 100644 index 0000000..e685891 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/api_client.py @@ -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 diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/env_config.py b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/env_config.py new file mode 100644 index 0000000..d575de6 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/env_config.py @@ -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(), + } diff --git a/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/logger_config.py b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/logger_config.py new file mode 100644 index 0000000..518c000 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/lzwcai_mcp_agile_db_third/utils/logger_config.py @@ -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() diff --git a/lzwcai_mcp_agile_db_third/main.py b/lzwcai_mcp_agile_db_third/main.py new file mode 100644 index 0000000..0abd25e --- /dev/null +++ b/lzwcai_mcp_agile_db_third/main.py @@ -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() diff --git a/lzwcai_mcp_agile_db_third/pyproject.toml b/lzwcai_mcp_agile_db_third/pyproject.toml new file mode 100644 index 0000000..4c9dc91 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/pyproject.toml @@ -0,0 +1,41 @@ +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "lzwcai-mcp-agile-db-third" +version = "0.1.5" +description = "MCP server for Agile DB third-party datasource APIs" +readme = "README.md" +requires-python = ">=3.12" +license = {text = "MIT"} +authors = [ + {name = "lzwcai", email = "your-email@example.com"}, +] +keywords = ["mcp", "datasource", "database", "agile-db"] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "httpx>=0.28.1", + "mcp[cli]>=1.10.1", +] + +[project.scripts] +lzwcai-mcp-agile-db-third = "lzwcai_mcp_agile_db_third.main:main" + +[tool.hatch.build.targets.wheel] +packages = ["lzwcai_mcp_agile_db_third"] + +[tool.hatch.build.targets.wheel.force-include] + +[tool.hatch.build] +exclude = [ + "lzwcai_mcp_agile_db_third/logs/**", + "**/.__pycache__/**", + "lzwcai_mcp_agile_db_third/.gitignore", + ".venv/**", +] diff --git a/lzwcai_mcp_agile_db_third/selftest_tools.py b/lzwcai_mcp_agile_db_third/selftest_tools.py new file mode 100644 index 0000000..18fabd3 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/selftest_tools.py @@ -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()) diff --git a/lzwcai_mcp_agile_db_third/uv.toml b/lzwcai_mcp_agile_db_third/uv.toml new file mode 100644 index 0000000..719a3e2 --- /dev/null +++ b/lzwcai_mcp_agile_db_third/uv.toml @@ -0,0 +1,4 @@ +[[index]] +name = "pypi" +url = "https://pypi.org/simple/" +default = true diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.log b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.log index 46fc2d7..adc5e4c 100644 --- a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.log +++ b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter.log @@ -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] - ================================================================================ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__pycache__/__init__.cpython-312.pyc index 1d71216..ef8a8fc 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__pycache__/__init__.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/__init__.cpython-312.pyc index 748969b..24df722 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/__init__.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/__init__.cpython-312.pyc index 7f75b3b..f457cb6 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/__init__.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc index a6a7be8..98c7801 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/business_util.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc index eb2aa08..454f833 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/business/__pycache__/get_business_api.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/__init__.cpython-312.pyc index c6c70a9..56e29c9 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/__init__.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc index 9237dde..0b38a7e 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_auth_service.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc index db83ca0..e59e4cc 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/api_base.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc index 8af1df1..0a96951 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/core_server.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc index a9d721e..e47af2e 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/core/__pycache__/get_auth.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/__init__.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/__init__.cpython-312.pyc index 882a706..ccef401 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/__init__.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/api_helper.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/api_helper.cpython-312.pyc index abe44b0..b239eb8 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/api_helper.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/api_helper.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc index 3c1dd23..d6f06e0 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/logger_config.cpython-312.pyc differ diff --git a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/nested_value.cpython-312.pyc b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/nested_value.cpython-312.pyc index 718f245..858c6f5 100644 Binary files a/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/nested_value.cpython-312.pyc and b/lzwcai_mcp_api_converter/lzwcai_mcp_api_converter/src/util/__pycache__/nested_value.cpython-312.pyc differ diff --git a/lzwcai_mcpskills_generate_reports/.gitignore b/lzwcai_mcpskills_generate_reports/.gitignore new file mode 100644 index 0000000..d36cd23 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/.gitignore @@ -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/ diff --git a/lzwcai_mcpskills_generate_reports/README.md b/lzwcai_mcpskills_generate_reports/README.md new file mode 100644 index 0000000..493f2ef --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/README.md @@ -0,0 +1,159 @@ +# lzwcai-mcpskills-generate-reports + +用户提供 **docx 模板 + JSON 数据**,本包负责渲染成 docx,并可选做样式迁移。 + +本包**不内置模板**,模板完全由调用方维护。 + +## 安装 + +```powershell +cd lzwcai_mcpskills_generate_reports +pip install -e . +``` + +## Python API + +### 渲染文档 + +```python +from lzwcai_mcpskills_generate_reports import generate + +out_path = generate( + data="data.json", # dict 或 JSON 文件路径 + template="./模板.docx", # 用户自己的 docx 模板路径 + out_path="_out/报价方案.docx", + style_ref="./用户样式.docx", # 可选:把用户模板的 theme/字体套过来 +) +``` + +### 扫描模板占位符 + +```python +from lzwcai_mcpskills_generate_reports import scan_template + +result = scan_template("./模板.docx") +print(result) +# { +# "placeholders": ["project_title", "contact_person", "equipments", ...], +# "blocks": [ +# {"type": "for", "iterator": "eq", "variable": "equipments"}, +# {"type": "if", "condition": "show_layout"}, +# ... +# ] +# } +``` + +## 命令行 + +```powershell +# 渲染 +generate-report generate --template ./模板.docx --data data.json --out _out/报价方案.docx + +# 扫描占位符 +generate-report scan --template ./模板.docx + +# 样式迁移 +generate-report generate --template ./模板.docx --data data.json --style-ref ./用户样式.docx --out _out/报价方案_定制.docx +``` + +## MCP Server + +本包同时提供 MCP Server(stdio 模式),把渲染引擎暴露成 3 个 MCP 工具: + +| 工具 | 说明 | 必填参数 | +|------|------|----------| +| `generate_report` | 模板 + 数据 → 渲染输出 docx,返回输出文件绝对路径 | `template`, `data`, `out`(可选 `style_ref`) | +| `scan_template` | 扫描模板占位符与 for/if 块结构 | `template` | +| `validate_report_data` | 校验数据契约(不渲染) | `data` | + +其中 `data` 既可以传 JSON 对象,也可以传指向 JSON 文件的路径字符串。 + +### 启动 + +```powershell +# 安装后用 console script 启动 +lzwcai-mcpskills-generate-reports + +# 或直接运行入口模块 +python main.py +``` + +### MCP 客户端配置示例 + +```json +{ + "mcpServers": { + "generate-reports": { + "command": "lzwcai-mcpskills-generate-reports" + } + } +} +``` + +> stdio 模式下 stdout 被 MCP 协议占用,所有日志写入 `logs/` 目录与 stderr。 + +## 数据契约(QuoteData) + +```json +{ + "project_title": "大米罐装线", + "contact_person": "张经理", + "contact_phone": "138-0000-0000", + "requirements": ["要求1", "要求2"], + "layout_image": "", + "layout_title": "整线布局尺寸图", + "equipments": [ + { + "index": "四", + "name": "自动理瓶机", + "images": [""], + "features": [{"title": "特点", "lines": ["说明"]}], + "params": [{"k": "材料", "v": "不锈钢"}] + } + ], + "quote_items": [ + {"name": "设备名", "qty": "一套", "image": "", "desc": "说明", "price": "面议"} + ] +} +``` + +图片字段约定:路径/URL → 真实图;`""` → 默认占位图;`None` → 不显示。 + +## 目录结构 + +``` +lzwcai_mcpskills_generate_reports/ +├── pyproject.toml +├── README.md +├── templates/ # 用户模板(示例,不在包内) +│ └── standard/ +│ ├── template.docx +│ └── meta.json +├── samples/ # 示例数据(不在包内) +│ └── sample_data.json +└── lzwcai_mcpskills_generate_reports/ # Python 包 + ├── __init__.py # 公共 API 入口 + ├── cli.py # 命令行 + ├── pipeline.py # 总入口 + ├── schema.py # 数据契约 + 校验器 + ├── render_quote.py # 渲染引擎 + ├── style_transfer.py # 样式迁移 + └── template_scanner.py # 模板占位符扫描 +``` + +## 模板约定 + +- 使用 [Jinja2](https://jinja.palletsprojects.com/) 语法写占位符,如 `{{ project_title }}`、`{% for eq in equipments %}`。 +- 模板文件旁的 `meta.json`(可选)声明图片字段宽度,例如: + +```json +{ + "image_fields": { + "layout_image": {"width_mm": 160}, + "equipments[].images[]": {"width_mm": 120}, + "quote_items[].image": {"width_mm": 30} + } +} +``` + +- BUSINESS 逻辑(内置模板维护、模板市场等)交给包外的上层系统处理。 diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/.python-version b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__init__.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__init__.py new file mode 100644 index 0000000..84f9022 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__init__.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" +lzwcai-mcpskills-generate-reports + +纯渲染引擎,不内置模板。 +对外暴露的公共 API: + - generate: 数据 + 模板路径 -> docx(核心入口) + - scan_template: 模板路径 -> 占位符 JSON + - validate: 校验数据契约 + - normalize: 归一化数据 + - transplant_style: 将用户模板样式迁移到结果文档 +""" + +__version__ = "0.1.0" + +from .pipeline import generate +from .template_scanner import scan_template +from .schema import validate, normalize, DEFAULTS +from .style_transfer import transplant_style + +__all__ = [ + "generate", + "scan_template", + "validate", + "normalize", + "transplant_style", + "DEFAULTS", + "__version__", +] diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/pipeline.cpython-312.pyc b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/pipeline.cpython-312.pyc new file mode 100644 index 0000000..0fee5cb Binary files /dev/null and b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/pipeline.cpython-312.pyc differ diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/render_quote.cpython-312.pyc b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/render_quote.cpython-312.pyc new file mode 100644 index 0000000..6c9bfae Binary files /dev/null and b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/render_quote.cpython-312.pyc differ diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/schema.cpython-312.pyc b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/schema.cpython-312.pyc new file mode 100644 index 0000000..d257a11 Binary files /dev/null and b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/schema.cpython-312.pyc differ diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/style_transfer.cpython-312.pyc b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/style_transfer.cpython-312.pyc new file mode 100644 index 0000000..5fd70be Binary files /dev/null and b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/style_transfer.cpython-312.pyc differ diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/cli.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/cli.py new file mode 100644 index 0000000..ba20c2c --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/cli.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +""" +cli.py:公共入口。 + +程序调用(推荐): + from lzwcai_mcpskills_generate_reports.cli import generate_report, scan_report + + generate_report( + template="./模板.docx", + data={"project_name": "x", ...}, + out="_out/a.docx", + ) + scan_report(template="./模板.docx") + +命令行: + generate-report generate --template ./模板.docx --data data.json --out _out/a.docx + generate-report scan --template ./模板.docx +""" +import argparse +import json +import os +import sys + +try: + sys.stdout.reconfigure(encoding="utf-8") +except (AttributeError, OSError): + pass + +from lzwcai_mcpskills_generate_reports import generate, scan_template + + +def _load_data(data): + """把 data 归一化为 dict:支持 dict 或 JSON 文件路径字符串。""" + if isinstance(data, str): + if not os.path.isfile(data): + raise FileNotFoundError(f"数据文件不存在: {data}") + with open(data, "r", encoding="utf-8") as f: + data = json.load(f) + + if not isinstance(data, dict): + raise TypeError(f"data 必须是 dict 或 JSON 文件路径字符串,实际类型: {type(data).__name__}") + return data + + +def generate_report(template, data, out, style_ref=None): + """生成 docx 报告。 + + 参数: + template: 模板 docx 文件路径 + data: dict 数据 或 JSON 文件路径字符串 + out: 输出 docx 文件路径 + style_ref: 用户样式参考 docx 路径(可选) + + 返回: + dict: {"output": 输出文件绝对路径} + """ + data = _load_data(data) + out = generate(data=data, template=template, out_path=out, style_ref=style_ref) + return {"output": out} + + +def scan_report(template): + """扫描模板占位符。 + + 参数: + template: 模板 docx 文件路径 + + 返回: + dict: 占位符扫描结果 + """ + return scan_template(template) + + +def _build_arg_parser(): + parser = argparse.ArgumentParser( + description="docx 模板渲染与占位符扫描", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +示例: + generate-report generate --template ./模板.docx --data data.json --out _out/a.docx + generate-report generate --template ./模板.docx --data data.json --style-ref 用户样式.docx --out _out/b.docx + generate-report scan --template ./模板.docx + """, + ) + sub = parser.add_subparsers(dest="command", required=True) + + gen_parser = sub.add_parser("generate", help="渲染生成 docx") + gen_parser.add_argument("--template", required=True, help="模板 docx 文件路径") + gen_parser.add_argument("--data", required=True, help="数据 JSON 文件路径") + gen_parser.add_argument("--out", required=True, help="输出 docx 文件路径") + gen_parser.add_argument("--style-ref", default=None, help="用户上传的样式参考 docx(可选)") + + scan_parser = sub.add_parser("scan", help="扫描模板占位符") + scan_parser.add_argument("--template", required=True, help="模板 docx 文件路径") + + return parser + + +def main(): + """命令行入口(console_scripts 调用)。""" + parser = _build_arg_parser() + args = parser.parse_args() + + try: + if args.command == "generate": + result = generate_report( + template=args.template, + data=args.data, + out=args.out, + style_ref=args.style_ref, + ) + print(f"生成成功: {result['output']}") + elif args.command == "scan": + result = scan_report(template=args.template) + print(json.dumps(result, ensure_ascii=False, indent=2)) + except Exception as e: + print(f"错误: {e}", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/logs/lzwcai_mcpskills_generate_reports.log b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/logs/lzwcai_mcpskills_generate_reports.log new file mode 100644 index 0000000..8ae8aa3 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/logs/lzwcai_mcpskills_generate_reports.log @@ -0,0 +1,37 @@ +2026-06-16 16:27:35 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs +2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-16 16:27:35 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具 +2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=validate_report_data, arguments={"data": "samples\\sample_data.json"} +2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: validate_report_data +2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=scan_template, arguments={"template": "templates\\standard\\template.docx"} +2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: scan_template +2026-06-16 16:28:01 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs +2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-16 16:28:01 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具 +2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=validate_report_data, arguments={"data": "samples\\sample_data.json"} +2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: validate_report_data +2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=scan_template, arguments={"template": "templates\\standard\\template.docx"} +2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: scan_template +2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=generate_report, arguments={"template": "templates\\standard\\template.docx", "data": "samples\\sample_data.json", "out": "_out\\mcp_test.docx"} +2026-06-16 16:28:20 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: generate_report +2026-06-17 10:11:02 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs +2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:215] - ================================================== +2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:216] - lzwcai-mcpskills-generate-reports MCP Server 启动 +2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:217] - ================================================== +2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:218] - 开始运行 MCP Server (stdio 模式) +2026-06-17 10:11:02 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') +2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-17 10:11:03 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-17 10:11:03 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具 +2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/logs/lzwcai_mcpskills_generate_reports_error.log b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/logs/lzwcai_mcpskills_generate_reports_error.log new file mode 100644 index 0000000..e69de29 diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/pipeline.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/pipeline.py new file mode 100644 index 0000000..891aeb5 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/pipeline.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +""" +pipeline.py:总入口 — 串起 数据校验 -> 渲染 -> (可选)样式迁移。 + +注意:本包不维护内置模板。调用方需要自己准备 .docx 模板文件, +并通过 template 参数传入路径。 +""" +import os +import json + +from .schema import validate, normalize +from .render_quote import render +from .style_transfer import transplant_style + + +def _load_data(data): + """把 data 归一化为 dict:支持 dict 或 JSON 文件路径。""" + if isinstance(data, str): + if not os.path.isfile(data): + raise FileNotFoundError(f"数据文件不存在: {data}") + with open(data, "r", encoding="utf-8") as f: + data = json.load(f) + + if not isinstance(data, dict): + raise TypeError(f"data 必须是 dict 或 JSON 文件路径,实际类型: {type(data).__name__}") + return data + + +def _load_meta_for_template(template_path): + """如果模板同目录下存在 meta.json,则读取;否则返回空 dict。""" + meta_file = os.path.join(os.path.dirname(template_path), "meta.json") + if os.path.isfile(meta_file): + with open(meta_file, "r", encoding="utf-8") as f: + return json.load(f) + return {} + + +def generate(data, template, out_path, style_ref=None): + """生成报价文档。 + + 参数: + data: QuoteData dict(或 JSON 文件路径字符串) + template: 模板 docx 文件路径 + out_path: 输出 docx 路径 + style_ref: 用户上传的样式参考 docx 路径(可选) + + 返回: + 生成的 docx 绝对路径 + """ + data = _load_data(data) + + if not isinstance(template, str): + raise TypeError(f"template 必须是文件路径字符串,实际类型: {type(template).__name__}") + if not os.path.isfile(template): + raise FileNotFoundError(f"模板文件不存在: {template}") + template_path = os.path.abspath(template) + + # 归一化 + 校验 + data = normalize(data) + errs = validate(data) + if errs: + raise ValueError("数据校验失败:\n" + "\n".join(errs)) + + # 确保输出目录存在 + out_dir = os.path.dirname(os.path.abspath(out_path)) + if out_dir: + os.makedirs(out_dir, exist_ok=True) + + # 渲染(优先读取同目录 meta.json 作为图片配置) + meta = _load_meta_for_template(template_path) + render(data, out_path, template_path, meta=meta) + + # 可选样式迁移 + if style_ref: + if not os.path.isfile(style_ref): + raise FileNotFoundError(f"样式参考文件不存在: {style_ref}") + transplant_style(out_path, style_ref, out_path) + + return os.path.abspath(out_path) diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/render_quote.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/render_quote.py new file mode 100644 index 0000000..691240e --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/render_quote.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- +""" +render_quote.py:渲染引擎 — data + 模板路径 -> docx。 + +从 reports4 移植,做了两处通用化改造: +1. 模板路径参数化: render(data, out_path, template_path) +2. 图片字段配置驱动: 读 meta.json 的 image_fields 声明,不再写死字段名 +""" +import copy +import os +import ssl +import sys +import tempfile +import urllib.request +import json + +try: + sys.stdout.reconfigure(encoding="utf-8") +except (AttributeError, OSError): + pass + +from docxtpl import DocxTemplate, InlineImage +from docx.shared import Mm + + +def _resolve_image_path(src, tmp_files): + """把图片字段值解析为本地文件路径。 + + 下载成功的临时文件会记录到 tmp_files,由调用方在渲染结束后统一清理。 + """ + def _download(url): + try: + ctx = ssl.create_default_context() + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + data = urllib.request.urlopen(req, context=ctx, timeout=30).read() + ext = os.path.splitext(url.split("?")[0])[1] or ".png" + fd, path = tempfile.mkstemp(suffix=ext) + os.write(fd, data) + os.close(fd) + tmp_files.append(path) + return path + except Exception as e: + print(f"[warn] 下载图片失败({url}): {e}") + return None + + if src is None: + return "" + if src == "": + return "" + if isinstance(src, str) and src.lower().startswith(("http://", "https://")): + return _download(src) or "" + if os.path.exists(src): + return src + print(f"[warn] 找不到图片({src}),跳过") + return "" + + +def _make_inline_image(tpl, src, width_mm, tmp_files): + """创建 InlineImage;src 为 "" 或 None 时不创建(返回 None)。""" + resolved = _resolve_image_path(src, tmp_files) + if resolved == "": + return None + return InlineImage(tpl, resolved, width=Mm(width_mm)) + + +def _resolve_field_path(data, path): + """根据字段路径提取所有目标值。 + + 支持: "root_field" / "list[].field" / "list[].field[]" + """ + results = [] + parts = path.split(".") + + if len(parts) == 1: + key = parts[0] + if key in data: + results.append((data, key)) + return results + + # 嵌套列表: "list[].field[]" —— 必须先于单层判断,否则会被 parts[0] 分支误捕获 + if len(parts) == 2 and "[]" in parts[1]: + list_name = parts[0].replace("[]", "") + field = parts[1].replace("[]", "") + for item in data.get(list_name, []): + lst = item.get(field, []) + if isinstance(lst, list): + for idx in range(len(lst)): + results.append((lst, idx)) + return results + + if len(parts) == 2 and "[]" in parts[0]: + list_name = parts[0].replace("[]", "") + field = parts[1] + for item in data.get(list_name, []): + if field in item: + results.append((item, field)) + return results + + if len(parts) == 2: + list_name = parts[0] + field = parts[1] + for item in data.get(list_name, []): + if field in item: + results.append((item, field)) + return results + + return results + + +def _fill_images_from_meta(tpl, data, meta, tmp_files): + """根据 meta.json 中的 image_fields 声明填充所有图片字段。""" + image_fields = meta.get("image_fields", {}) + for path, width_meta in image_fields.items(): + width_mm = width_meta.get("width_mm", 50) + refs = _resolve_field_path(data, path) + for container, key in refs: + val = container[key] + if val is None: + container[key] = "" + elif isinstance(val, list): + container[key] = [ + _make_inline_image(tpl, x, width_mm, tmp_files) if x is not None else None + for x in val + ] + container[key] = [img for img in container[key] if img is not None] + else: + img = _make_inline_image(tpl, val, width_mm, tmp_files) + container[key] = img if img else "" + return data + + +def render(data, out_path, template_path, meta=None): + """渲染报价文档。""" + if meta is None: + meta_dir = os.path.dirname(template_path) + meta_file = os.path.join(meta_dir, "meta.json") + if os.path.isfile(meta_file): + with open(meta_file, "r", encoding="utf-8") as f: + meta = json.load(f) + else: + meta = {} + + # 深拷贝:避免修改调用方传入的数据,同时把字符串字段替换成 InlineImage + data = copy.deepcopy(dict(data)) + tpl = DocxTemplate(template_path) + + tmp_files = [] + try: + data = _fill_images_from_meta(tpl, data, meta, tmp_files) + tpl.render(data) + tpl.save(out_path) + finally: + for p in tmp_files: + try: + os.remove(p) + except OSError: + pass + + return out_path diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/schema.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/schema.py new file mode 100644 index 0000000..ebc97da --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/schema.py @@ -0,0 +1,123 @@ +# -*- coding: utf-8 -*- +""" +schema.py:QuoteData 数据契约、校验器、归一化。 + +所有模板共用同一契约,沿用 reports4 的 SAMPLE 字段结构。 +""" +import copy + +# ── 缺省值表 ────────────────────────────────────────────── +DEFAULTS = { + "layout_title": "整线布局尺寸图", + "show_layout": True, +} + +# 中文数字扩展(用于缺 index 时自动补) +_CN_DIGITS = [ + "一", "二", "三", "四", "五", "六", "七", "八", "九", "十", + "十一", "十二", "十三", "十四", "十五", "十六", "十七", "十八", "十九", "二十", +] + +# 设备之前的固定章节数:一 公司简介、二 客户要求及分析、三 布局图 +_SECTION_OFFSET = 3 + + +def validate(data): + """校验 QuoteData 结构,返回错误信息列表(空=通过)。 + + 检查: + - 必填顶层字段: project_title, contact_person, contact_phone, requirements + - equipments 元素: 必须有 name + - quote_items 元素: 必须有 name + - features 元素: 必须有 title 和 lines + - params 元素: 必须有 k 和 v + """ + errors = [] + + for field in ("project_title", "contact_person", "contact_phone", "requirements"): + val = data.get(field) + if val is None or (isinstance(val, str) and val.strip() == ""): + errors.append(f"必填字段缺失或为空: {field}") + + # equipments + eqs = data.get("equipments", []) + if not isinstance(eqs, list): + errors.append("equipments 必须为列表") + else: + for i, eq in enumerate(eqs): + if not isinstance(eq, dict): + errors.append(f"equipments[{i}] 必须为对象") + continue + if not eq.get("name"): + errors.append(f"equipments[{i}] 缺少 name") + # features 子元素 + for j, feat in enumerate(eq.get("features", [])): + if not isinstance(feat, dict): + errors.append(f"equipments[{i}].features[{j}] 必须为对象") + continue + if not feat.get("title"): + errors.append(f"equipments[{i}].features[{j}] 缺少 title") + if not feat.get("lines") or not isinstance(feat.get("lines"), list): + errors.append(f"equipments[{i}].features[{j}] 缺少 lines 或类型错误") + # params 子元素 + for j, p in enumerate(eq.get("params", [])): + if not isinstance(p, dict): + errors.append(f"equipments[{i}].params[{j}] 必须为对象") + continue + if not p.get("k") or not p.get("v"): + errors.append(f"equipments[{i}].params[{j}] 缺少 k 或 v") + + # quote_items + items = data.get("quote_items", []) + if not isinstance(items, list): + errors.append("quote_items 必须为列表") + else: + for i, it in enumerate(items): + if not isinstance(it, dict): + errors.append(f"quote_items[{i}] 必须为对象") + continue + if not it.get("name"): + errors.append(f"quote_items[{i}] 缺少 name") + + return errors + + +def normalize(data): + """归一化数据:填缺省值、requirements list->str、index 缺失时自动补。 + + 不修改入参,返回新副本。 + """ + d = copy.deepcopy(data) + + # requirements: list -> str + req = d.get("requirements") + if isinstance(req, (list, tuple)): + d["requirements"] = "\n".join(str(x) for x in req) + + # 缺省值 + for k, v in DEFAULTS.items(): + if k not in d: + d[k] = v + + # equipment index 自动补(从"四"开始,前面三节是公司简介/客户要求/布局图) + for i, eq in enumerate(d.get("equipments", [])): + if not eq.get("index"): + idx = i + 1 + _SECTION_OFFSET + eq["index"] = _CN_DIGITS[idx - 1] if idx <= len(_CN_DIGITS) else str(idx) + + # quote_items 缺 image 给空串 + for it in d.get("quote_items", []): + it.setdefault("image", "") + + # equipments 缺 images 给 [""] + for eq in d.get("equipments", []): + eq.setdefault("images", [""]) + + # 计算后续章节的动态序号:报价表 & 售后服务 + eq_count = len(d.get("equipments", [])) + quote_idx = eq_count + _SECTION_OFFSET + 1 + after_sales_idx = eq_count + _SECTION_OFFSET + 2 + d["section_quote_table"] = _CN_DIGITS[quote_idx - 1] if quote_idx <= len(_CN_DIGITS) else str(quote_idx) + d["section_after_sales"] = _CN_DIGITS[after_sales_idx - 1] if after_sales_idx <= len(_CN_DIGITS) else str(after_sales_idx) + + return d diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/server.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/server.py new file mode 100644 index 0000000..0368f42 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/server.py @@ -0,0 +1,223 @@ +#!/usr/bin/env python3 +""" +lzwcai-mcpskills-generate-reports MCP Server + +把 docx 模板渲染引擎封装成 MCP 工具,提供三个工具: +- generate_report: 数据 + 模板路径 -> 渲染输出 docx +- scan_template: 扫描模板占位符 / for / if 块 +- validate_report_data: 校验数据契约(不渲染) + +stdio 模式运行;所有日志走 stderr,stdout 留给 MCP 协议。 +""" +import os +import json +import logging + +import anyio + +import mcp.types as types +from mcp.server import NotificationOptions, Server +from mcp.server.models import InitializationOptions +from mcp.server.stdio import stdio_server + +try: + from .utils.logger_config import setup_system_logging, get_logger + from . import generate, scan_template, validate, normalize +except ImportError: + from lzwcai_mcpskills_generate_reports.utils.logger_config import ( + setup_system_logging, get_logger, + ) + from lzwcai_mcpskills_generate_reports import ( + generate, scan_template, validate, normalize, + ) + +# 初始化日志系统 +setup_system_logging(app_name="lzwcai_mcpskills_generate_reports", log_level=logging.DEBUG) +logger = get_logger(__name__) + +# 初始化 MCP Server +server = Server("lzwcai_mcpskills_generate_reports") + + +def _load_data(data): + """把 data 归一化为 dict:支持 dict 或 JSON 文件路径字符串。""" + if isinstance(data, str): + if not os.path.isfile(data): + raise FileNotFoundError(f"数据文件不存在: {data}") + with open(data, "r", encoding="utf-8") as f: + data = json.load(f) + if not isinstance(data, dict): + raise TypeError( + f"data 必须是对象或 JSON 文件路径字符串,实际类型: {type(data).__name__}" + ) + return data + + +# ── 工具定义 ────────────────────────────────────────────── +_DATA_DESC = "报价数据:可以是 JSON 对象,或指向 JSON 文件的路径字符串" + +TOOL_DEFS = [ + types.Tool( + name="generate_report", + description=( + "用 docx 模板 + 结构化数据渲染生成报价文档 docx,返回输出文件绝对路径。" + "模板由调用方提供,本工具不内置模板。" + ), + inputSchema={ + "type": "object", + "properties": { + "template": { + "type": "string", + "description": "模板 docx 文件路径(必填)", + }, + "data": { + "type": ["object", "string"], + "description": _DATA_DESC + "(必填)", + }, + "out": { + "type": "string", + "description": "输出 docx 文件路径(必填)", + }, + "style_ref": { + "type": "string", + "description": "用户上传的样式参考 docx 路径(可选),会把其 theme/字体套到结果文档", + }, + }, + "required": ["template", "data", "out"], + }, + ), + types.Tool( + name="scan_template", + description=( + "扫描 docx 模板中的 Jinja2 占位符,返回需要外部提供的顶层变量列表和 for/if 块结构。" + "用于在渲染前了解模板需要哪些数据字段。" + ), + inputSchema={ + "type": "object", + "properties": { + "template": { + "type": "string", + "description": "模板 docx 文件路径(必填)", + }, + }, + "required": ["template"], + }, + ), + types.Tool( + name="validate_report_data", + description=( + "校验报价数据是否符合契约(必填字段、equipments/quote_items/features/params 结构)," + "不渲染文档。返回校验结果和错误列表。" + ), + inputSchema={ + "type": "object", + "properties": { + "data": { + "type": ["object", "string"], + "description": _DATA_DESC + "(必填)", + }, + }, + "required": ["data"], + }, + ), +] + + +@server.list_tools() +async def handle_list_tools() -> list[types.Tool]: + """列出所有可用工具""" + logger.info(f"收到 ListTools 请求,返回 {len(TOOL_DEFS)} 个工具") + return TOOL_DEFS + + +# ── 工具实现(同步函数,放线程池执行)────────────────────── +def _do_generate_report(arguments: dict) -> dict: + template = arguments["template"] + data = arguments["data"] + out = arguments["out"] + style_ref = arguments.get("style_ref") + + data = _load_data(data) + out_path = generate(data=data, template=template, out_path=out, style_ref=style_ref) + return {"output": out_path} + + +def _do_scan_template(arguments: dict) -> dict: + return scan_template(arguments["template"]) + + +def _do_validate(arguments: dict) -> dict: + data = _load_data(arguments["data"]) + norm = normalize(data) + errors = validate(norm) + return {"valid": not errors, "errors": errors} + + +_HANDLERS = { + "generate_report": _do_generate_report, + "scan_template": _do_scan_template, + "validate_report_data": _do_validate, +} + + +@server.call_tool() +async def handle_call_tool( + name: str, + arguments: dict | None, +) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: + """调用工具""" + logger.info( + f"收到 CallTool 请求: name={name}, " + f"arguments={json.dumps(arguments, ensure_ascii=False) if arguments else 'None'}" + ) + + handler = _HANDLERS.get(name) + if handler is None: + logger.error(f"未找到工具: {name}") + raise ValueError(f"未知工具: {name}") + + args = arguments or {} + try: + # 渲染/扫描是同步阻塞的 CPU/IO 任务,放线程池避免霸占 event loop + result = await anyio.to_thread.run_sync(handler, args) + logger.info(f"工具执行成功: {name}") + except Exception as e: + logger.error(f"工具执行失败: {name}: {e}", exc_info=True) + result = {"error": str(e), "tool_name": name} + + return [ + types.TextContent( + type="text", + text=json.dumps(result, ensure_ascii=False, indent=2), + ) + ] + + +async def run_server(): + """运行 MCP Server (stdio 模式)""" + async with stdio_server() as streams: + await server.run( + streams[0], + streams[1], + InitializationOptions( + server_name="lzwcai_mcpskills_generate_reports", + server_version="0.1.0", + capabilities=server.get_capabilities( + notification_options=NotificationOptions(), + experimental_capabilities={}, + ), + ), + ) + + +def main(): + """主入口""" + logger.info("=" * 50) + logger.info("lzwcai-mcpskills-generate-reports MCP Server 启动") + logger.info("=" * 50) + logger.info("开始运行 MCP Server (stdio 模式)") + anyio.run(run_server) + + +if __name__ == "__main__": + main() diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/style_transfer.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/style_transfer.py new file mode 100644 index 0000000..81e1534 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/style_transfer.py @@ -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 diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/template_scanner.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/template_scanner.py new file mode 100644 index 0000000..fd854b4 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/template_scanner.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +""" +template_scanner.py:扫描 docx 模板中的 Jinja2 占位符。 + +只读模板,不渲染;返回模板里要求外部提供的数据字段清单。 +""" +import re +import zipfile +import xml.etree.ElementTree as ET + +from jinja2 import Environment, meta +from jinja2 import nodes as jinja_nodes + + +def _iter_docx_text(docx_path): + """遍历 docx 中所有 XML 文本节点,产出原始字符串片段。""" + with zipfile.ZipFile(docx_path, "r") as z: + for name in z.namelist(): + # 只关心 word 主文档、页眉、页脚 + if not (name.startswith("word/document") or name.startswith("word/header") or name.startswith("word/footer")): + continue + data = z.read(name) + try: + root = ET.fromstring(data) + except ET.ParseError: + continue + # w:t 节点存放文本 + for elem in root.iter(): + if elem.tag.endswith("}t"): + if elem.text: + yield elem.text + + +def _extract_source(docx_path): + """把 docx 所有文本片段拼成一段连续的源文本,便于 Jinja2 解析。""" + return "\n".join(_iter_docx_text(docx_path)) + + +def _normalize_docxtpl_tags(source): + """把 docxtpl 段落/行/单元格级标签 {%p ... %} {%tr ... %} {%tc ... %} 还原为 {% ... %}。 + + docxtpl 用 {%p、{%tr、{%tc 控制块作用于段落、表格行、表格单元格; + 扫描占位符时不需要这些粒度信息,统一成标准 Jinja2 标签即可解析。 + """ + return re.sub(r"{%\s*(?:p|tr|tc)\s+", "{% ", source) + + +def _walk_blocks(node, result=None): + """遍历 Jinja2 AST,收集 For / If 块信息。""" + if result is None: + result = [] + if node is None: + return result + + if isinstance(node, jinja_nodes.For): + iter_name = _expression_name(node.iter) + target_name = _expression_name(node.target) + result.append({ + "type": "for", + "iterator": target_name, + "variable": iter_name, + }) + for child in node.body: + _walk_blocks(child, result) + for child in node.else_ or []: + _walk_blocks(child, result) + return result + + if isinstance(node, jinja_nodes.If): + test_name = _expression_name(node.test) + result.append({ + "type": "if", + "condition": test_name, + }) + for child in node.body: + _walk_blocks(child, result) + for child in node.else_ or []: + _walk_blocks(child, result) + return result + + if hasattr(node, "body"): + for child in node.body: + _walk_blocks(child, result) + + if hasattr(node, "else_") and node.else_: + for child in node.else_: + _walk_blocks(child, result) + + return result + + +def _expression_name(expr): + """把 Jinja2 表达式尽量还原为可读的字符串。""" + if expr is None: + return None + if isinstance(expr, jinja_nodes.Name): + return expr.name + if isinstance(expr, jinja_nodes.Const): + return str(expr.value) + if isinstance(expr, jinja_nodes.Getattr): + return f"{_expression_name(expr.node)}.{expr.attr}" + if isinstance(expr, jinja_nodes.Getitem): + return f"{_expression_name(expr.node)}[{_expression_name(expr.arg)}]" + return str(expr) + + +def scan_template(template_path): + """扫描 docx 模板,返回占位符信息 JSON。 + + 返回: + { + "placeholders": ["project_title", "equipments", ...], # 需要外部提供的最顶层变量 + "blocks": [ + {"type": "for", "iterator": "eq", "variable": "equipments"}, + {"type": "if", "condition": "show_layout"}, + ... + ] + } + """ + source = _normalize_docxtpl_tags(_extract_source(template_path)) + if not source.strip(): + return {"placeholders": [], "blocks": []} + + env = Environment() + ast = env.parse(source) + + variables = sorted(meta.find_undeclared_variables(ast)) + blocks = _walk_blocks(ast) + + return { + "placeholders": variables, + "blocks": blocks, + } diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/__init__.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/__init__.py new file mode 100644 index 0000000..d7805be --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/__init__.py @@ -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"] diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/logger_config.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/logger_config.py new file mode 100644 index 0000000..f70c1a6 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/logger_config.py @@ -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) diff --git a/lzwcai_mcpskills_generate_reports/main.py b/lzwcai_mcpskills_generate_reports/main.py new file mode 100644 index 0000000..7946a46 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/main.py @@ -0,0 +1,10 @@ +""" +Entry point for lzwcai-mcpskills-generate-reports + +Runs the MCP server (stdio mode) for docx report generation. +""" + +from lzwcai_mcpskills_generate_reports.server import main + +if __name__ == "__main__": + main() diff --git a/lzwcai_mcpskills_generate_reports/pyproject.toml b/lzwcai_mcpskills_generate_reports/pyproject.toml new file mode 100644 index 0000000..f63b8aa --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/pyproject.toml @@ -0,0 +1,40 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "lzwcai-mcpskills-generate-reports" +version = "0.1.0" +description = "Render styled quotation documents from user-supplied docx templates and structured data" +readme = "README.md" +requires-python = ">=3.12" +keywords = ["docx", "quotation", "report", "template", "jinja2"] +authors = [ + { name = "LzwCai", email = "lzwcai@example.com" }, +] +classifiers = [ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", +] +dependencies = [ + "docxtpl>=0.16.0", + "python-docx>=1.1.0", + "Jinja2>=3.1.0", + "mcp>=1.0.0", + "anyio>=4.0.0", +] + +[project.scripts] +generate-report = "lzwcai_mcpskills_generate_reports.cli:main" +lzwcai-mcpskills-generate-reports = "lzwcai_mcpskills_generate_reports.server:main" + +[project.urls] +Homepage = "https://github.com/lzwcai/lzwcai-mcpskills-generate-reports" +Repository = "https://github.com/lzwcai/lzwcai-mcpskills-generate-reports" + +[tool.setuptools.packages.find] +where = ["."] +include = ["lzwcai_mcpskills_generate_reports*"] +exclude = ["tests*", "_out*", "templates*", "samples*"] diff --git a/lzwcai_mcpskills_generate_reports/samples/sample_data.json b/lzwcai_mcpskills_generate_reports/samples/sample_data.json new file mode 100644 index 0000000..3c49eda --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/samples/sample_data.json @@ -0,0 +1,412 @@ +{ + "project_title": "大米圆形纸罐全自动灌装包装整线项目", + "contact_person": "张卫国经理", + "contact_phone": "138-1568-9632", + "contact_company": "XX粮油食品有限公司", + "requirements": [ + "包装物料:成品精制大米,流动性颗粒物料;单罐净重规格:250g、500g、1000g三档快速切换。", + "罐体参数:圆形食品级纸罐,配套易拉预封口+外旋螺纹盖双重密封;纸罐固定外径100mm(10cm),高度分3档:60mm(6cm)/120mm(12cm)/220mm(22cm),同线兼容三规格自动换型无需更换夹具。", + "额定包装速度:以500g标准罐计,稳定产能≥20罐/分钟,连续24h不间断运行无卡罐、漏装。", + "整线标配:大米定量称重灌装机、罐身输送线、自动上盖机、四轮旋盖机、成品出料输送机;", + "客户选配完整模组:在线金属检测机(带自动剔除机构)+重量复检机(不合格自动分流剔除)+后端自动开箱机+折盖封箱一体机+机器人码垛单元;", + "全线材质要求:物料接触部位304不锈钢,符合食品QS/SC生产卫生规范,支持水洗清洁。" + ], + "layout_image": "https://dscache.tencent-cloud.cn/upload/nodir/rice-can-line-layout-202605.png", + "layout_title": "大米纸罐灌装整线平面布局尺寸总图", + "equipments": [ + { + "name": "Z型大米颗粒上料提升机", + "images": [ + "https://dscache.tencent-cloud.cn/upload/nodir/z-conveyor-01.png", + "https://dscache.tencent-cloud.cn/upload/nodir/z-conveyor-detail.png" + ], + "features": [ + { + "title": "粮食专用密封提升", + "lines": [ + "封闭式料斗输送,无大米撒料、扬尘,车间粉尘达标;适配大米、杂粮等颗粒原料连续上料。", + "料斗加厚304不锈钢,耐磨抗冲击,不易积粮霉变,便于高压水枪冲洗。" + ] + }, + { + "title": "变频调速稳定可控", + "lines": [ + "独立变频电机调速,上料流量与灌装主机信号联动匹配,不会断料或溢料。", + "低噪音链条传动,连续运行故障率低,维护简单,可长期满负荷生产。" + ] + } + ], + "params": [ + { + "k": "有效提升高度", + "v": "3.6m(支持现场按需加长定制)" + }, + { + "k": "输送速度可调范围", + "v": "0~16m/min" + }, + { + "k": "最大输送产能", + "v": "7.2m³/h,满足25罐/分钟灌装余量" + }, + { + "k": "驱动总功率", + "v": "0.55kW" + }, + { + "k": "供电制式", + "v": "380V三相 50Hz" + }, + { + "k": "整机净重", + "v": "385kg" + }, + { + "k": "物料接触材质", + "v": "304食品级不锈钢" + } + ] + }, + { + "name": "多头称重式大米灌装机", + "images": [ + "https://dscache.tencent-cloud.cn/upload/nodir/rice-filling-machine.png" + ], + "features": [ + { + "title": "高精度称重灌装", + "lines": [ + "二级快慢加料,250g/500g/1000g重量参数触摸屏一键调用,单罐称重误差≤±0.8g。", + "独立称重料斗,不受物料料位高低影响,长时间灌装重量一致性稳定。" + ] + }, + { + "title": "食品级卫生设计", + "lines": [ + "料仓、下料口快拆结构,无需工具即可拆卸清洗,无死角存粮。", + "整机带防尘外封板,避免蚊虫、杂物混入大米成品。" + ] + } + ], + "params": [ + { + "k": "灌装量程", + "v": "100~1200g可调" + }, + { + "k": "额定产能", + "v": "22~28罐/分钟(500g规格)" + }, + { + "k": "允许罐型外径", + "v": "60~120mm" + }, + { + "k": "整机功率", + "v": "1.2kW" + }, + { + "k": "外形长宽高", + "v": "1450×920×1850mm" + }, + { + "k": "整机重量", + "v": "420kg" + }, + { + "k": "操作界面", + "v": "7寸触摸屏+PLC全自动控制" + } + ] + }, + { + "name": "自动纸罐理罐机", + "images": [ + "https://dscache.tencent-cloud.cn/upload/nodir/can-sorter.png" + ], + "features": [ + { + "title": "圆形纸罐定向排序", + "lines": [ + "自动散乱上罐、扶正定位、有序输送,杜绝倒罐、卡罐,适配本次100mm外径纸罐三高度规格。", + "定位挡板手摇快速调节,换型时间≤3分钟。" + ] + }, + { + "title": "整机耐用易维护", + "lines": [ + "机架304不锈钢,输送带独立无极变频调速,可和灌装主机速度同步联动。", + "机械结构简洁,易损件少,车间操作工可独立日常检修。" + ] + } + ], + "params": [ + { + "k": "主体材质", + "v": "304不锈钢机架+食品级PU输送带" + }, + { + "k": "整机外形尺寸", + "v": "1020×810×1220mm" + }, + { + "k": "最大处理产能", + "v": "30~50罐/分钟" + }, + { + "k": "理罐转盘直径", + "v": "800mm" + }, + { + "k": "适配罐体外径", + "v": "60~200mm" + }, + { + "k": "工作电压", + "v": "单相220V,50Hz" + }, + { + "k": "整机重量", + "v": "145kg" + } + ] + }, + { + "name": "自动上盖机+单头四轮旋盖一体机", + "images": [ + "https://dscache.tencent-cloud.cn/upload/nodir/capping-twist-machine.png" + ], + "features": [ + { + "title": "自动送盖+定位旋盖一体化", + "lines": [ + "料仓自动整理螺纹外盖,分盖、落盖精准套入罐口,四轮橡胶轮柔性夹紧旋紧,不会压扁纸质罐口。", + "旋盖扭矩数字可调,杜绝滑盖、拧过紧纸罐变形,适配易拉内封+外旋盖双层封口工艺。" + ] + }, + { + "title": "速度同步联动", + "lines": [ + "变频调速跟随灌装线主线速度,无空罐漏旋,缺盖自动停机报警提示补料。" + ] + } + ], + "params": [ + { + "k": "供电规格", + "v": "220V 50Hz" + }, + { + "k": "整机装机功率", + "v": "1.0kW" + }, + { + "k": "适配罐口直径", + "v": "35~130mm" + }, + { + "k": "稳定旋盖速度", + "v": "25~32罐/分钟" + }, + { + "k": "设备外形尺寸", + "v": "2020×660×1510mm" + }, + { + "k": "整机重量", + "v": "295kg" + }, + { + "k": "扭矩调节方式", + "v": "数字扭矩电控调节" + } + ] + }, + { + "name": "在线金属检测机(带剔除)", + "images": [ + "https://dscache.tencent-cloud.cn/upload/nodir/metal-detector-reject.png" + ], + "features": [ + { + "title": "高精度金属异物检测", + "lines": [ + "可检出铁、不锈钢、铜、铝等混入大米内金属碎屑、螺钉刀片,不合格罐气动自动剔除分流,不混入合格品。", + "检测灵敏度数字可调,产品记忆存储,多规格一键切换。" + ] + } + ], + "params": [ + { + "k": "检测通道尺寸", + "v": "宽140mm×高280mm" + }, + { + "k": "检测灵敏度", + "v": "Feφ1.0mm、SUSφ2.2mm" + }, + { + "k": "剔除方式", + "v": "气动推杆自动剔除" + }, + { + "k": "适配线速", + "v": "0~30m/min" + }, + { + "k": "功率", + "v": "0.37kW" + } + ] + }, + { + "name": "成品重量复检机", + "images": [ + "https://dscache.tencent-cloud.cn/upload/nodir/checkweigher.png" + ], + "features": [ + { + "title": "缺料超重自动分选", + "lines": [ + "在线动态称重,低于下限、高于上限罐体自动剔除,杜绝少装、多装次品流入装箱工序。" + ] + } + ], + "params": [ + { + "k": "称重量程", + "v": "0~2000g" + }, + { + "k": "称重精度", + "v": "±0.3g" + }, + { + "k": "剔除方式", + "v": "气动拨杆剔除" + } + ] + }, + { + "name": "后端自动开箱+折盖封箱一体机", + "images": [ + "https://dscache.tencent-cloud.cn/upload/nodir/case-sealer.png" + ], + "features": [ + { + "title": "纸箱成型封底+上盖折平封箱一次完成", + "lines": [ + "整垛纸箱自动吸取撑开、底部胶带封牢,成品罐装箱后自动折左右上盖,上下工字封箱,适配整线连续自动化装箱。" + ] + } + ], + "params": [ + { + "k": "适用纸箱尺寸范围", + "v": "长250~450×宽180~320×高150~400mm" + }, + { + "k": "封箱速度", + "v": "6~12箱/分钟" + }, + { + "k": "总功率", + "v": "1.8kW" + } + ] + }, + { + "name": "机械臂码垛机", + "images": [ + "https://dscache.tencent-cloud.cn/upload/nodir/robot-palletizer.png" + ], + "features": [ + { + "title": "纸箱自动堆叠码垛", + "lines": [ + "伺服抓手抓取成品整箱,按预设垛型整齐码放在托盘上,码垛高度、层数程序可调,替代人工堆垛。" + ] + } + ], + "params": [ + { + "k": "最大负载", + "v": "25kg/箱" + }, + { + "k": "码垛高度上限", + "v": "1800mm" + }, + { + "k": "工作节拍", + "v": "8~12箱/分钟" + } + ] + } + ], + "quote_items": [ + { + "name": "Z型大米上料提升机", + "qty": "1套", + "image": "https://dscache.tencent-cloud.cn/upload/nodir/z-conveyor-01.png", + "desc": "304不锈钢封闭式粮食提升,变频调速,配套灌装主机联动上料", + "price": "面议" + }, + { + "name": "多头称重大米灌装机", + "qty": "1套", + "image": "https://dscache.tencent-cloud.cn/upload/nodir/rice-filling-machine.png", + "desc": "三规格重量一键切换,高精度称重下料,食品级快拆清洗结构", + "price": "面议" + }, + { + "name": "自动纸罐理罐机", + "qty": "1套", + "image": "https://dscache.tencent-cloud.cn/upload/nodir/can-sorter.png", + "desc": "圆形纸罐自动排序扶正,适配Φ100mm三高度纸罐快速换型", + "price": "面议" + }, + { + "name": "自动上盖+四轮旋盖一体机", + "qty": "1套", + "image": "https://dscache.tencent-cloud.cn/upload/nodir/capping-twist-machine.png", + "desc": "自动分盖上盖,数字扭矩旋紧,适配纸罐易拉盖+外旋盖双层封口", + "price": "面议" + }, + { + "name": "金属检测机(带剔除)", + "qty": "1套", + "image": "https://dscache.tencent-cloud.cn/upload/nodir/metal-detector-reject.png", + "desc": "在线金属异物检测,不合格罐体自动剔除分流,食品生产合规必备", + "price": "面议" + }, + { + "name": "重量复检剔除机", + "qty": "1套", + "image": "https://dscache.tencent-cloud.cn/upload/nodir/checkweigher.png", + "desc": "动态在线称重,超重欠重次品自动剔除,保证净含量达标", + "price": "面议" + }, + { + "name": "自动开箱折盖封箱一体机", + "qty": "1套", + "image": "https://dscache.tencent-cloud.cn/upload/nodir/case-sealer.png", + "desc": "纸箱自动成型封底、装箱后折盖封箱,后端自动化装箱配套", + "price": "面议" + }, + { + "name": "机器人码垛单元", + "qty": "1套", + "image": "https://dscache.tencent-cloud.cn/upload/nodir/robot-palletizer.png", + "desc": "抓取成品纸箱自动码垛堆托,解放后端人工搬运堆叠", + "price": "面议" + }, + { + "name": "全线不锈钢输送过渡机架+电控总控制柜", + "qty": "1套", + "image": "https://dscache.tencent-cloud.cn/upload/nodir/line-conveyor-total.png", + "desc": "各设备接驳输送线、整机联动PLC总控制系统、急停、报警、联动互锁整套电气配套", + "price": "面议" + } + ] +} diff --git a/lzwcai_mcpskills_generate_reports/templates/standard/meta.json b/lzwcai_mcpskills_generate_reports/templates/standard/meta.json new file mode 100644 index 0000000..8e8706a --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/templates/standard/meta.json @@ -0,0 +1,11 @@ +{ + "name": "标准罐装线模板", + "description": "默认模板,适用于大多数罐装线报价方案", + "image_fields": { + "layout_image": {"width_mm": 160, "scope": "root"}, + "equipments[].images[]": {"width_mm": 120, "scope": "list"}, + "quote_items[].image": {"width_mm": 30, "scope": "list"} + }, + "required": ["project_title", "contact_person", "contact_phone", "requirements"], + "optional_sections": ["show_layout"] +} diff --git a/lzwcai_mcpskills_generate_reports/templates/standard/template.docx b/lzwcai_mcpskills_generate_reports/templates/standard/template.docx new file mode 100644 index 0000000..2e7ba92 Binary files /dev/null and b/lzwcai_mcpskills_generate_reports/templates/standard/template.docx differ diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/main.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/main.cpython-312.pyc index 2aad1c1..f9375df 100644 Binary files a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/main.cpython-312.pyc and b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/main.cpython-312.pyc differ diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/schema_converter.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/schema_converter.cpython-312.pyc index 83efa78..60317e8 100644 Binary files a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/schema_converter.cpython-312.pyc and b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/__pycache__/schema_converter.cpython-312.pyc differ diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/lzwcai_workflow_to_mcp.log b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/lzwcai_workflow_to_mcp.log index cb9382c..df84e1e 100644 --- a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/lzwcai_workflow_to_mcp.log +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/lzwcai_workflow_to_mcp.log @@ -1,137 +1,129 @@ -2026-03-28 12:30:33 - root - INFO - [logger_config.py:117] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_workflow_to_mcp\lzwcai_workflow_to_mcp\logs -2026-03-28 12:30:33 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'workflow_mcp_server' -2026-03-28 12:30:33 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest -2026-03-28 12:30:33 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:334] - ================================================== -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:335] - Workflow MCP Server 启动 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:336] - ================================================== -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:341] - 命令行参数: {'mode': 'api', 'json_path': None, 'workflow_id': None} -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:344] - 使用模式: api -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:103] - ApiLoader 初始化,工作流ID: 2037527155235225601 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:111] - 开始从 API 加载工作流配置,工作流ID: 2037527155235225601 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:115] - [客户端初始化] base_url=http://192.168.2.236:8088 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:116] - [客户端初始化] token=wf_bd39a583670c42ceab48b3353bf2ba43 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:117] - [客户端初始化] execute_timeout=600.0s -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:148] - [API请求] GET http://192.168.2.236:8088/system/workflowManage/getByWorkflowId/2037527155235225601 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:149] - [API请求] Headers: {'X-API-Key': 'wf_bd39a583670c42ceab48b3353bf2ba43'} -2026-03-28 12:30:34 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None -2026-03-28 12:30:34 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - 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'Sat, 28 Mar 2026 04:30:30 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')]) -2026-03-28 12:30:34 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/system/workflowManage/getByWorkflowId/2037527155235225601 "HTTP/1.1 200 " -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:156] - [API响应] HTTP 200 -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:157] - [API响应] Headers: {'vary': 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers', 'x-content-type-options': 'nosniff', 'x-xss-protection': '1; mode=block', 'x-frame-options': 'SAMEORIGIN', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'date': 'Sat, 28 Mar 2026 04:30:30 GMT', 'keep-alive': 'timeout=60', 'connection': 'keep-alive'} -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:162] - [API响应] 获取工作流配置成功: workflow_id=2037527155235225601 -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:163] - [API响应] Body: { +2026-06-17 10:01:21 - root - INFO - [logger_config.py:117] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_workflow_to_mcp\lzwcai_workflow_to_mcp\logs +2026-06-17 10:01:21 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'workflow_mcp_server' +2026-06-17 10:01:21 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-17 10:01:21 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:341] - ================================================== +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:342] - Workflow MCP Server 启动 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:343] - ================================================== +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:348] - 命令行参数: {'mode': 'api', 'json_path': None, 'workflow_id': None} +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:351] - 使用模式: api +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:103] - ApiLoader 初始化,工作流ID: 2059567220704460801 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:111] - 开始从 API 加载工作流配置,工作流ID: 2059567220704460801 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:115] - [客户端初始化] base_url=http://192.168.2.236:8088 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:116] - [客户端初始化] token=wf_3782054fa0d345df973dedf0775d98e7 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:117] - [客户端初始化] execute_timeout=600.0s +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:148] - [API请求] GET http://192.168.2.236:8088/system/workflowManage/getByWorkflowId/2059567220704460801 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:149] - [API请求] Headers: {'X-API-Key': 'wf_3782054fa0d345df973dedf0775d98e7'} +2026-06-17 10:01:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None +2026-06-17 10:01:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - 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'Wed, 17 Jun 2026 02:01:19 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')]) +2026-06-17 10:01:21 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/system/workflowManage/getByWorkflowId/2059567220704460801 "HTTP/1.1 200 " +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:156] - [API响应] HTTP 200 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:157] - [API响应] Headers: {'vary': 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers', 'x-content-type-options': 'nosniff', 'x-xss-protection': '1; mode=block', 'x-frame-options': 'SAMEORIGIN', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'date': 'Wed, 17 Jun 2026 02:01:19 GMT', 'keep-alive': 'timeout=60', 'connection': 'keep-alive'} +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:162] - [API响应] 获取工作流配置成功: workflow_id=2059567220704460801 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:163] - [API响应] Body: { "msg": "查询成功", "code": 200, "data": [ { - "id": "2037535460665982977", + "id": "2059567233220263937", "createBy": "duchangyuan", - "createTime": "2026-03-27 22:21:24", - "updateBy": "admin", - "updateTime": "2026-03-28 12:21:49", - "serviceId": "2037535460632428546", - "uniqueName": "银行流水状态检测", - "name": "yinxingliushuizhuangtaijiance_7c4d9a63", - "description": "用于检测流水导入状态,检查是否全部导入完成", + "createTime": "2026-05-27 17:27:48", + "updateBy": "yy8z9", + "updateTime": "2026-06-17 00:08:32", + "serviceId": "2059567233190903810", + "uniqueName": "文件导入(支持压缩包)", + "name": "wenjiandaoruzhichiyasuobao_cdde86e3", + "description": "dd", "visualizable": 0, "toolPrompt": null, "toolType": "api", "datasourceId": null, "sqlTemplate": null, - "sqlParams": "[]", + "sqlParams": "[{\"type\":\"string\",\"name\":\"file\",\"displayName\":\"原文件路径\",\"maxLength\":3000,\"required\":false,\"defaultValue\":\"\"},{\"type\":\"string\",\"name\":\"txt_file\",\"displayName\":\"解析后txt文件路径\",\"required\":true,\"defaultValue\":\"\"}]", "resultType": null, "sourceType": null, "trainingTaskId": null, "tableMetadataIds": null, "executionCount": 0, "visualizationConfigs": null, - "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[]}", + "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"file\":{\"description\":\"原文件路径\",\"type\":\"string\",\"maxLength\":3000},\"txt_file\":{\"description\":\"解析后txt文件路径\",\"type\":\"string\"},\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[\"txt_file\"]}", "outputJsonSchema": "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}},\"additionalProperties\":false}", "lastExecutionTime": null } ] } -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:116] - API 响应原始数据: { +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:116] - API 响应原始数据: { "msg": "查询成功", "code": 200, "data": [ { - "id": "2037535460665982977", + "id": "2059567233220263937", "createBy": "duchangyuan", - "createTime": "2026-03-27 22:21:24", - "updateBy": "admin", - "updateTime": "2026-03-28 12:21:49", - "serviceId": "2037535460632428546", - "uniqueName": "银行流水状态检测", - "name": "yinxingliushuizhuangtaijiance_7c4d9a63", - "description": "用于检测流水导入状态,检查是否全部导入完成", + "createTime": "2026-05-27 17:27:48", + "updateBy": "yy8z9", + "updateTime": "2026-06-17 00:08:32", + "serviceId": "2059567233190903810", + "uniqueName": "文件导入(支持压缩包)", + "name": "wenjiandaoruzhichiyasuobao_cdde86e3", + "description": "dd", "visualizable": 0, "toolPrompt": null, "toolType": "api", "datasourceId": null, "sqlTemplate": null, - "sqlParams": "[]", + "sqlParams": "[{\"type\":\"string\",\"name\":\"file\",\"displayName\":\"原文件路径\",\"maxLength\":3000,\"required\":false,\"defaultValue\":\"\"},{\"type\":\"string\",\"name\":\"txt_file\",\"displayName\":\"解析后txt文件路径\",\"required\":true,\"defaultValue\":\"\"}]", "resultType": null, "sourceType": null, "trainingTaskId": null, "tableMetadataIds": null, "executionCount": 0, "visualizationConfigs": null, - "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[]}", + "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"file\":{\"description\":\"原文件路径\",\"type\":\"string\",\"maxLength\":3000},\"txt_file\":{\"description\":\"解析后txt文件路径\",\"type\":\"string\"},\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[\"txt_file\"]}", "outputJsonSchema": "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}},\"additionalProperties\":false}", "lastExecutionTime": null } ] } -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:124] - API 响应 data 字段: [ +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:124] - API 响应 data 字段: [ { - "id": "2037535460665982977", + "id": "2059567233220263937", "createBy": "duchangyuan", - "createTime": "2026-03-27 22:21:24", - "updateBy": "admin", - "updateTime": "2026-03-28 12:21:49", - "serviceId": "2037535460632428546", - "uniqueName": "银行流水状态检测", - "name": "yinxingliushuizhuangtaijiance_7c4d9a63", - "description": "用于检测流水导入状态,检查是否全部导入完成", + "createTime": "2026-05-27 17:27:48", + "updateBy": "yy8z9", + "updateTime": "2026-06-17 00:08:32", + "serviceId": "2059567233190903810", + "uniqueName": "文件导入(支持压缩包)", + "name": "wenjiandaoruzhichiyasuobao_cdde86e3", + "description": "dd", "visualizable": 0, "toolPrompt": null, "toolType": "api", "datasourceId": null, "sqlTemplate": null, - "sqlParams": "[]", + "sqlParams": "[{\"type\":\"string\",\"name\":\"file\",\"displayName\":\"原文件路径\",\"maxLength\":3000,\"required\":false,\"defaultValue\":\"\"},{\"type\":\"string\",\"name\":\"txt_file\",\"displayName\":\"解析后txt文件路径\",\"required\":true,\"defaultValue\":\"\"}]", "resultType": null, "sourceType": null, "trainingTaskId": null, "tableMetadataIds": null, "executionCount": 0, "visualizationConfigs": null, - "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[]}", + "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"file\":{\"description\":\"原文件路径\",\"type\":\"string\",\"maxLength\":3000},\"txt_file\":{\"description\":\"解析后txt文件路径\",\"type\":\"string\"},\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[\"txt_file\"]}", "outputJsonSchema": "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}},\"additionalProperties\":false}", "lastExecutionTime": null } ] -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - INFO - [main.py:135] - 从 API 加载工作流配置成功,工作流ID: 2037527155235225601, 配置数量: 1 -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - INFO - [main.py:165] - 已加载 1 个工具配置 -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - INFO - [main.py:352] - 开始运行 MCP Server (stdio 模式) -2026-03-28 12:30:34 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor -2026-03-28 12:30:34 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') -2026-03-28 12:30:40 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: -2026-03-28 12:30:40 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest -2026-03-28 12:30:40 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest -2026-03-28 12:30:40 - lzwcai_workflow_to_mcp.main - INFO - [main.py:171] - 收到 ListTools 请求,当前配置数量: 1 -2026-03-28 12:30:40 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:178] - 处理工具配置: name=yinxingliushuizhuangtaijiance_7c4d9a63, description=用于检测流水导入状态,检查是否全部导入完成... -2026-03-28 12:30:40 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:185] - 从 sqlParams 转换的 inputSchema: {"type": "object", "properties": {}, "required": []} -2026-03-28 12:30:40 - lzwcai_workflow_to_mcp.main - INFO - [main.py:237] - ListTools 响应: 返回 1 个工具 -2026-03-28 12:30:40 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:135] - 从 API 加载工作流配置成功,工作流ID: 2059567220704460801, 配置数量: 1 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:165] - 已加载 1 个工具配置 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:359] - 开始运行 MCP Server (stdio 模式) +2026-06-17 10:01:21 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-17 10:01:21 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/lzwcai_workflow_to_mcp_daily.log b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/lzwcai_workflow_to_mcp_daily.log index cb9382c..df84e1e 100644 --- a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/lzwcai_workflow_to_mcp_daily.log +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/logs/lzwcai_workflow_to_mcp_daily.log @@ -1,137 +1,129 @@ -2026-03-28 12:30:33 - root - INFO - [logger_config.py:117] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_workflow_to_mcp\lzwcai_workflow_to_mcp\logs -2026-03-28 12:30:33 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'workflow_mcp_server' -2026-03-28 12:30:33 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest -2026-03-28 12:30:33 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:334] - ================================================== -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:335] - Workflow MCP Server 启动 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:336] - ================================================== -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:341] - 命令行参数: {'mode': 'api', 'json_path': None, 'workflow_id': None} -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:344] - 使用模式: api -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:103] - ApiLoader 初始化,工作流ID: 2037527155235225601 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.main - INFO - [main.py:111] - 开始从 API 加载工作流配置,工作流ID: 2037527155235225601 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:115] - [客户端初始化] base_url=http://192.168.2.236:8088 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:116] - [客户端初始化] token=wf_bd39a583670c42ceab48b3353bf2ba43 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:117] - [客户端初始化] execute_timeout=600.0s -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:148] - [API请求] GET http://192.168.2.236:8088/system/workflowManage/getByWorkflowId/2037527155235225601 -2026-03-28 12:30:33 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:149] - [API请求] Headers: {'X-API-Key': 'wf_bd39a583670c42ceab48b3353bf2ba43'} -2026-03-28 12:30:34 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None -2026-03-28 12:30:34 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - 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'Sat, 28 Mar 2026 04:30:30 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')]) -2026-03-28 12:30:34 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/system/workflowManage/getByWorkflowId/2037527155235225601 "HTTP/1.1 200 " -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request= -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started -2026-03-28 12:30:34 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:156] - [API响应] HTTP 200 -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:157] - [API响应] Headers: {'vary': 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers', 'x-content-type-options': 'nosniff', 'x-xss-protection': '1; mode=block', 'x-frame-options': 'SAMEORIGIN', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'date': 'Sat, 28 Mar 2026 04:30:30 GMT', 'keep-alive': 'timeout=60', 'connection': 'keep-alive'} -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:162] - [API响应] 获取工作流配置成功: workflow_id=2037527155235225601 -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:163] - [API响应] Body: { +2026-06-17 10:01:21 - root - INFO - [logger_config.py:117] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_workflow_to_mcp\lzwcai_workflow_to_mcp\logs +2026-06-17 10:01:21 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'workflow_mcp_server' +2026-06-17 10:01:21 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-17 10:01:21 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:341] - ================================================== +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:342] - Workflow MCP Server 启动 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:343] - ================================================== +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:348] - 命令行参数: {'mode': 'api', 'json_path': None, 'workflow_id': None} +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:351] - 使用模式: api +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:103] - ApiLoader 初始化,工作流ID: 2059567220704460801 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:111] - 开始从 API 加载工作流配置,工作流ID: 2059567220704460801 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:115] - [客户端初始化] base_url=http://192.168.2.236:8088 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:116] - [客户端初始化] token=wf_3782054fa0d345df973dedf0775d98e7 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:117] - [客户端初始化] execute_timeout=600.0s +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:148] - [API请求] GET http://192.168.2.236:8088/system/workflowManage/getByWorkflowId/2059567220704460801 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:149] - [API请求] Headers: {'X-API-Key': 'wf_3782054fa0d345df973dedf0775d98e7'} +2026-06-17 10:01:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None +2026-06-17 10:01:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - 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'Wed, 17 Jun 2026 02:01:19 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')]) +2026-06-17 10:01:21 - httpx - INFO - [_client.py:1025] - HTTP Request: GET http://192.168.2.236:8088/system/workflowManage/getByWorkflowId/2059567220704460801 "HTTP/1.1 200 " +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request= +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started +2026-06-17 10:01:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:156] - [API响应] HTTP 200 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:157] - [API响应] Headers: {'vary': 'Origin, Access-Control-Request-Method, Access-Control-Request-Headers', 'x-content-type-options': 'nosniff', 'x-xss-protection': '1; mode=block', 'x-frame-options': 'SAMEORIGIN', 'content-type': 'application/json', 'transfer-encoding': 'chunked', 'date': 'Wed, 17 Jun 2026 02:01:19 GMT', 'keep-alive': 'timeout=60', 'connection': 'keep-alive'} +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - INFO - [api_client.py:162] - [API响应] 获取工作流配置成功: workflow_id=2059567220704460801 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.utils.api_client - DEBUG - [api_client.py:163] - [API响应] Body: { "msg": "查询成功", "code": 200, "data": [ { - "id": "2037535460665982977", + "id": "2059567233220263937", "createBy": "duchangyuan", - "createTime": "2026-03-27 22:21:24", - "updateBy": "admin", - "updateTime": "2026-03-28 12:21:49", - "serviceId": "2037535460632428546", - "uniqueName": "银行流水状态检测", - "name": "yinxingliushuizhuangtaijiance_7c4d9a63", - "description": "用于检测流水导入状态,检查是否全部导入完成", + "createTime": "2026-05-27 17:27:48", + "updateBy": "yy8z9", + "updateTime": "2026-06-17 00:08:32", + "serviceId": "2059567233190903810", + "uniqueName": "文件导入(支持压缩包)", + "name": "wenjiandaoruzhichiyasuobao_cdde86e3", + "description": "dd", "visualizable": 0, "toolPrompt": null, "toolType": "api", "datasourceId": null, "sqlTemplate": null, - "sqlParams": "[]", + "sqlParams": "[{\"type\":\"string\",\"name\":\"file\",\"displayName\":\"原文件路径\",\"maxLength\":3000,\"required\":false,\"defaultValue\":\"\"},{\"type\":\"string\",\"name\":\"txt_file\",\"displayName\":\"解析后txt文件路径\",\"required\":true,\"defaultValue\":\"\"}]", "resultType": null, "sourceType": null, "trainingTaskId": null, "tableMetadataIds": null, "executionCount": 0, "visualizationConfigs": null, - "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[]}", + "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"file\":{\"description\":\"原文件路径\",\"type\":\"string\",\"maxLength\":3000},\"txt_file\":{\"description\":\"解析后txt文件路径\",\"type\":\"string\"},\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[\"txt_file\"]}", "outputJsonSchema": "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}},\"additionalProperties\":false}", "lastExecutionTime": null } ] } -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:116] - API 响应原始数据: { +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:116] - API 响应原始数据: { "msg": "查询成功", "code": 200, "data": [ { - "id": "2037535460665982977", + "id": "2059567233220263937", "createBy": "duchangyuan", - "createTime": "2026-03-27 22:21:24", - "updateBy": "admin", - "updateTime": "2026-03-28 12:21:49", - "serviceId": "2037535460632428546", - "uniqueName": "银行流水状态检测", - "name": "yinxingliushuizhuangtaijiance_7c4d9a63", - "description": "用于检测流水导入状态,检查是否全部导入完成", + "createTime": "2026-05-27 17:27:48", + "updateBy": "yy8z9", + "updateTime": "2026-06-17 00:08:32", + "serviceId": "2059567233190903810", + "uniqueName": "文件导入(支持压缩包)", + "name": "wenjiandaoruzhichiyasuobao_cdde86e3", + "description": "dd", "visualizable": 0, "toolPrompt": null, "toolType": "api", "datasourceId": null, "sqlTemplate": null, - "sqlParams": "[]", + "sqlParams": "[{\"type\":\"string\",\"name\":\"file\",\"displayName\":\"原文件路径\",\"maxLength\":3000,\"required\":false,\"defaultValue\":\"\"},{\"type\":\"string\",\"name\":\"txt_file\",\"displayName\":\"解析后txt文件路径\",\"required\":true,\"defaultValue\":\"\"}]", "resultType": null, "sourceType": null, "trainingTaskId": null, "tableMetadataIds": null, "executionCount": 0, "visualizationConfigs": null, - "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[]}", + "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"file\":{\"description\":\"原文件路径\",\"type\":\"string\",\"maxLength\":3000},\"txt_file\":{\"description\":\"解析后txt文件路径\",\"type\":\"string\"},\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[\"txt_file\"]}", "outputJsonSchema": "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}},\"additionalProperties\":false}", "lastExecutionTime": null } ] } -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:124] - API 响应 data 字段: [ +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:124] - API 响应 data 字段: [ { - "id": "2037535460665982977", + "id": "2059567233220263937", "createBy": "duchangyuan", - "createTime": "2026-03-27 22:21:24", - "updateBy": "admin", - "updateTime": "2026-03-28 12:21:49", - "serviceId": "2037535460632428546", - "uniqueName": "银行流水状态检测", - "name": "yinxingliushuizhuangtaijiance_7c4d9a63", - "description": "用于检测流水导入状态,检查是否全部导入完成", + "createTime": "2026-05-27 17:27:48", + "updateBy": "yy8z9", + "updateTime": "2026-06-17 00:08:32", + "serviceId": "2059567233190903810", + "uniqueName": "文件导入(支持压缩包)", + "name": "wenjiandaoruzhichiyasuobao_cdde86e3", + "description": "dd", "visualizable": 0, "toolPrompt": null, "toolType": "api", "datasourceId": null, "sqlTemplate": null, - "sqlParams": "[]", + "sqlParams": "[{\"type\":\"string\",\"name\":\"file\",\"displayName\":\"原文件路径\",\"maxLength\":3000,\"required\":false,\"defaultValue\":\"\"},{\"type\":\"string\",\"name\":\"txt_file\",\"displayName\":\"解析后txt文件路径\",\"required\":true,\"defaultValue\":\"\"}]", "resultType": null, "sourceType": null, "trainingTaskId": null, "tableMetadataIds": null, "executionCount": 0, "visualizationConfigs": null, - "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[]}", + "inputJsonSchema": "{\"type\":\"object\",\"properties\":{\"file\":{\"description\":\"原文件路径\",\"type\":\"string\",\"maxLength\":3000},\"txt_file\":{\"description\":\"解析后txt文件路径\",\"type\":\"string\"},\"workflow_extraContext\":{\"description\":\"工作流额外的上下文参数(如环境变量等),可以是任何类型,非必填\"}},\"required\":[\"txt_file\"]}", "outputJsonSchema": "{\"type\":\"object\",\"properties\":{\"text\":{\"type\":\"string\"}},\"additionalProperties\":false}", "lastExecutionTime": null } ] -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - INFO - [main.py:135] - 从 API 加载工作流配置成功,工作流ID: 2037527155235225601, 配置数量: 1 -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - INFO - [main.py:165] - 已加载 1 个工具配置 -2026-03-28 12:30:34 - lzwcai_workflow_to_mcp.main - INFO - [main.py:352] - 开始运行 MCP Server (stdio 模式) -2026-03-28 12:30:34 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor -2026-03-28 12:30:34 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') -2026-03-28 12:30:40 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: -2026-03-28 12:30:40 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest -2026-03-28 12:30:40 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest -2026-03-28 12:30:40 - lzwcai_workflow_to_mcp.main - INFO - [main.py:171] - 收到 ListTools 请求,当前配置数量: 1 -2026-03-28 12:30:40 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:178] - 处理工具配置: name=yinxingliushuizhuangtaijiance_7c4d9a63, description=用于检测流水导入状态,检查是否全部导入完成... -2026-03-28 12:30:40 - lzwcai_workflow_to_mcp.main - DEBUG - [main.py:185] - 从 sqlParams 转换的 inputSchema: {"type": "object", "properties": {}, "required": []} -2026-03-28 12:30:40 - lzwcai_workflow_to_mcp.main - INFO - [main.py:237] - ListTools 响应: 返回 1 个工具 -2026-03-28 12:30:40 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:135] - 从 API 加载工作流配置成功,工作流ID: 2059567220704460801, 配置数量: 1 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:165] - 已加载 1 个工具配置 +2026-06-17 10:01:21 - lzwcai_workflow_to_mcp.main - INFO - [main.py:359] - 开始运行 MCP Server (stdio 模式) +2026-06-17 10:01:21 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-17 10:01:21 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/main.py b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/main.py index 9f6a2e9..bff9c82 100644 --- a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/main.py +++ b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/main.py @@ -288,10 +288,17 @@ async def handle_call_tool( try: # 调用工作流执行API - result = execute_workflow(request_data) + # 注意:execute_workflow 内部是同步阻塞调用(同步 httpx.Client + + # poll_workflow_result 的 time.sleep 轮询)。直接在 async handler 里调用, + # 会在工作流执行的整个时长内(最长 600s)霸占单线程 asyncio 的 event loop, + # 导致 MCP SDK 为其他请求 start_soon 出来的 task(list_tools / ping / + # 其他 call_tool)全部拿不到 CPU 时间片而饿死——表现为高并发下其他会话 + # 的 preflight/list_tools 超时(ConnectionError: ... TimeoutError)。 + # 放到线程池执行后,event loop 立即解放,可继续并发处理其他请求。 + result = await anyio.to_thread.run_sync(execute_workflow, request_data) logger.info(f"工作流执行成功: {workflow_id}") logger.debug(f"工作流执行结果: {json.dumps(result, ensure_ascii=False, indent=2)}") - + # 提取 final_output 最后一个节点的值 result = extract_final_output(result) except Exception as e: diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/__init__.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/__init__.cpython-312.pyc index 074d8be..29950e4 100644 Binary files a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/__init__.cpython-312.pyc and b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/api_client.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/api_client.cpython-312.pyc index d25d8eb..f8671d1 100644 Binary files a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/api_client.cpython-312.pyc and b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/api_client.cpython-312.pyc differ diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/env_config.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/env_config.cpython-312.pyc index 0c048b2..397ed13 100644 Binary files a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/env_config.cpython-312.pyc and b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/env_config.cpython-312.pyc differ diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/json_helper.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/json_helper.cpython-312.pyc index 8aad326..6df6e62 100644 Binary files a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/json_helper.cpython-312.pyc and b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/json_helper.cpython-312.pyc differ diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/logger_config.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/logger_config.cpython-312.pyc index 1da07dc..28bad4a 100644 Binary files a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/logger_config.cpython-312.pyc and b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/logger_config.cpython-312.pyc differ diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/name_helper.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/name_helper.cpython-312.pyc index b61c36d..c2260ae 100644 Binary files a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/name_helper.cpython-312.pyc and b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/name_helper.cpython-312.pyc differ diff --git a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/schema_helper.cpython-312.pyc b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/schema_helper.cpython-312.pyc index 9eb072d..840e2eb 100644 Binary files a/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/schema_helper.cpython-312.pyc and b/lzwcai_workflow_to_mcp/lzwcai_workflow_to_mcp/utils/__pycache__/schema_helper.cpython-312.pyc differ diff --git a/lzwcai_workflow_to_mcp/main.py b/lzwcai_workflow_to_mcp/main.py index 18c43bd..7d53a2e 100644 --- a/lzwcai_workflow_to_mcp/main.py +++ b/lzwcai_workflow_to_mcp/main.py @@ -7,8 +7,8 @@ import os if __name__ == "__main__": # 设置环境变量 - os.environ["workflowId"] = "2037527155235225601" - os.environ["workflowExecuteKey"] = "wf_bd39a583670c42ceab48b3353bf2ba43" + os.environ["workflowId"] = "2059567220704460801" + os.environ["workflowExecuteKey"] = "wf_3782054fa0d345df973dedf0775d98e7" os.environ["backendBaseUrl"] = "http://192.168.2.236:8088" # Import and run the actual MCP server diff --git a/lzwcai_workflow_to_mcp/pyproject.toml b/lzwcai_workflow_to_mcp/pyproject.toml index 8389973..99fc244 100644 --- a/lzwcai_workflow_to_mcp/pyproject.toml +++ b/lzwcai_workflow_to_mcp/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "lzwcai-workflow-to-mcp" -version = "0.1.9" +version = "0.1.10" description = "MCP server for executing business SQL queries with dynamic tool generation" readme = "README.md" requires-python = ">=3.10"