From 635313a7abc39397b0c31bca6795d1cfb17aa87e Mon Sep 17 00:00:00 2001 From: yuanzhipeng <2501363769@qq.com> Date: Fri, 26 Jun 2026 16:21:41 +0800 Subject: [PATCH] =?UTF-8?q?```=20feat(lzwcai-agile-db):=20=E6=9B=B4?= =?UTF-8?q?=E6=96=B0=E7=89=88=E6=9C=AC=E8=87=B30.4.4=E5=B9=B6=E4=BC=98?= =?UTF-8?q?=E5=8C=96=E6=95=B0=E6=8D=AE=E5=BA=93=E7=AE=A1=E7=90=86=E6=8A=80?= =?UTF-8?q?=E8=83=BD=E6=96=87=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 更新版本号从0.4.2到0.4.4 - 优化API密钥权限管理说明,明确grant_api_key_permissions仅支持追加不支持撤销 - 新增add_sql_tool_to_datasource工具,提供一键创建SQL工具功能 - 调整create_sql_tool说明,强调需技能已存在 - 强化数据写操作安全机制,插入/更新/删除前必须预览并等待用户确认 - 完善导入数据功能说明,详细解释confirm_import_data参数传递方式 - 补充技能与工具管理流程,提供更清晰的操作指引 - 新增数字员工平台数据库技能配置指南文档 ``` --- .kilo/skills/lzwcai-agile-db.zip | Bin 0 -> 16913 bytes .kilo/skills/lzwcai-agile-db/SKILL.md | 175 ++- .kilo/skills/数字员工平台数据库技能的.md | 343 +++++ lzwcai_mcp_agile_db/README.md | 8 +- .../__pycache__/server.cpython-312.pyc | Bin 6276 -> 6275 bytes .../logs/lzwcai_mcp_agile_db.log | 387 +++++- .../logs/lzwcai_mcp_agile_db_error.log | 3 +- .../lzwcai_mcp_agile_db/server.py | 2 +- .../__pycache__/api_keys.cpython-312.pyc | Bin 7243 -> 5984 bytes .../__pycache__/data_import.cpython-312.pyc | Bin 8935 -> 15306 bytes .../tools/__pycache__/skills.cpython-312.pyc | Bin 7867 -> 17292 bytes .../lzwcai_mcp_agile_db/tools/api_keys.py | 38 +- .../lzwcai_mcp_agile_db/tools/data_import.py | 152 ++- .../lzwcai_mcp_agile_db/tools/skills.py | 271 +++- lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt | 1139 +++++++++++++++++ .../lzwcai_mcp_agile_db/utils/__init__.py | 10 +- .../__pycache__/__init__.cpython-312.pyc | Bin 456 -> 592 bytes .../__pycache__/api_client.cpython-312.pyc | Bin 12896 -> 20270 bytes .../__pycache__/env_config.cpython-312.pyc | Bin 2181 -> 2870 bytes .../lzwcai_mcp_agile_db/utils/api_client.py | 357 ++++-- .../lzwcai_mcp_agile_db/utils/env_config.py | 38 +- .../数据库管理平台-MCP工具设计方案.md | 32 +- lzwcai_mcp_agile_db/main.py | 6 +- lzwcai_mcp_agile_db/pyproject.toml | 2 +- lzwcai_mcp_agile_db_third/pyproject.toml | 4 +- lzwcai_mcpskills_generate_reports/README.md | 111 +- .../__init__.py | 10 +- .../__pycache__/pipeline.cpython-312.pyc | Bin 4112 -> 4868 bytes .../__pycache__/render_quote.cpython-312.pyc | Bin 7520 -> 7307 bytes .../__pycache__/schema.cpython-312.pyc | Bin 5816 -> 8198 bytes .../style_transfer.cpython-312.pyc | Bin 8814 -> 8744 bytes .../lzwcai_mcpskills_generate_reports/cli.py | 122 -- .../lzwcai_mcpskills_generate_reports.log | 276 +++- ...zwcai_mcpskills_generate_reports_error.log | 78 ++ .../lzwcai_mcpskills_generate_reports/main.py | 115 ++ .../pipeline.py | 40 +- .../render_quote.py | 7 +- .../schema.py | 75 +- .../server.py | 120 +- .../template_scanner.py | 136 +- .../utils/fetch.py | 77 ++ lzwcai_mcpskills_generate_reports/main.py | 12 +- .../pyproject.toml | 4 +- 43 files changed, 3464 insertions(+), 686 deletions(-) create mode 100644 .kilo/skills/lzwcai-agile-db.zip create mode 100644 .kilo/skills/数字员工平台数据库技能的.md create mode 100644 lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt delete mode 100644 lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/cli.py create mode 100644 lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/main.py create mode 100644 lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/fetch.py diff --git a/.kilo/skills/lzwcai-agile-db.zip b/.kilo/skills/lzwcai-agile-db.zip new file mode 100644 index 0000000000000000000000000000000000000000..d405ac1d2276fc96ac5642038ef931f762282381 GIT binary patch literal 16913 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 🔒 **写操作统一规则(插入 / 更新 / 删除都适用)**:在调用 `insert_table_row` / `update_table_row` / `delete_table_rows` 之前,**必须先把将要写入/修改/删除的具体数据预览给用户,并等待用户明确确认后才执行**。不得在用户未确认的情况下直接落库。 + ### 4.1 插入数据 ``` @@ -457,9 +461,11 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 ↓ 2. 确认必填字段(非空字段、无默认值字段) ↓ -3. 调用 insert_table_row(tableId="xx", data={...}) +3. 向用户展示将要插入的数据,询问"确认插入以上数据?",等待用户确认 ↓ -4. 确认插入成功 +4. 用户确认后,调用 insert_table_row(tableId="xx", data={...}) + ↓ +5. 确认插入成功 ``` **示例**: @@ -470,6 +476,12 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 调用: get_table_detail(tableId="5") 返回: {columns: [{name: "id", isPrimaryKey: true, isAutoIncrement: true}, {name: "username", isNullable: false}, ...]} +回复: 即将向 users 表插入以下数据: + username = test_user, email = test@test.com + (id 自动生成)。确认插入吗? + +用户: "确认" + 调用: insert_table_row( tableId="5", data={"username": "test_user", "email": "test@test.com"} @@ -484,9 +496,11 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 ↓ 1. 确认主键字段和要更新的值 ↓ -2. 调用 update_table_row(tableId="xx", primaryKey={主键}, data={要更新的字段}) +2. 向用户展示「目标行 + 改动前→改动后」的预览,询问"确认更新?",等待用户确认 ↓ -3. 确认更新成功 +3. 用户确认后,调用 update_table_row(tableId="xx", primaryKey={主键}, data={要更新的字段}) + ↓ +4. 确认更新成功 ``` **示例**: @@ -494,6 +508,12 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 **用户**: "把 ID 为 5 的用户邮箱改成 new@test.com" ``` +回复: 即将更新 users 表 ID=5 的记录: + email: old@test.com → new@test.com + 确认更新吗? + +用户: "确认" + 调用: update_table_row( tableId="5", primaryKey={"id": 5}, @@ -566,7 +586,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 ``` ### 注意事项 -- **删除操作必须二次确认** +- **写操作(插入/更新/删除)执行前必须先预览数据并等待用户确认**;删除操作不可恢复,需格外谨慎 - `primaryKey` 必须是对象格式,如 `{"id": 1}` - `primaryKeys` 是数组格式,如 `[{"id": 1}, {"id": 2}]` - `data` 只包含要更新的字段,不需要提供全部字段 @@ -711,6 +731,47 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 6. 确认导入成功(返回含 insertedRows 插入行数) ``` +### confirm_import_data 的 data 传什么 + +把 `preview_import_data` 返回的 data 原文整块传给 `data` 参数即可,工具会自动解包组装。data 的标准形态(= preview 返回): + +```json +{ + "tableStructure": { + "success": true, + "message": "Excel表结构生成成功", + "data": { + "tables": [ + { + "tableName": "animals", + "tableComment": "宠物信息表", + "columns": [ + { "columnName": "id", "columnType": "SERIAL", "isPrimaryKey": true, "isAdditionField": true }, + { "columnName": "animal_name", "columnType": "VARCHAR", "columnLength": 5000 } + ] + } + ] + }, + "allData": [ + ["id", "animal_name", "..."], + ["1", "豆豆", "..."], + ["2", "咪咪", "..."] + ] + }, + "databaseName": "pp_test", + "target": "prod" +} +``` + +- `tableStructure`:preview 的表结构包装(`{success, message, data:{tables:[...]}}`),工具会取 `tables[0]` 当单表对象,并把 `databaseName` 塞进去。 +- `databaseName` / `target` 既可放顶层参数,也可放在 `data` 里,工具都认。 +- ⚠️ **allData 是二维数组,首行必须是「列名表头行」**: + - `allData[0]` = 各列的 `columnName`(列名表头),**真实数据从 `allData[1]` 起**; + - 每行(含表头行)都是按 `columns` 顺序排列的位置数组,**行宽 = 列总数**(含 `SERIAL` 主键等所有列,**不裁剪**,主键列给占位值即可,后端自增时会忽略); + - 若传入的 allData 没带表头行(首行就是数据),工具会**据列名自动补一行表头**——否则后端会把首行数据当成字段名,报「查询字段不存在/字段名称不正确」。 +- ⚠️ **columns / 表头列名必须对应目标表真实存在的字段**:后端按列名拼 INSERT,`tableStructure.columns`(以及 allData 首行表头)里的列名必须是目标表里**确实存在的字段名**,否则报「查询字段不存在 / 字段名称不正确」。 + - **导入到已有表时**:不要直接用 Excel 识别出的列,应**先 `get_table_detail(tableId="xx")` 拿到目标表真实字段,再把 data 里的 columns、allData 首行表头、各行取值都对齐到这些真实字段**(前端就是用目标表真实列覆盖 AI 识别列的)。 + ### 注意事项 - 文件大小限制:< 500KB - 支持格式:.xlsx / .xls @@ -720,6 +781,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 - `file_url` 对应的文件扩展名需为 `.xlsx` / `.xls` - **`confirm_import_data` 必须传 `databaseName`**(落库目标库);`data` 直接传 `preview_import_data` 的返回原文即可,工具内部会自动解包并组装成 `{tableStructure(含databaseName), allData}` - AI 识别会把中文表头转成英文列名(如「姓名」→`name`);若导入数据键名与生成的列名对不上会报「未找到 XX 字段」,此时需按预览返回的列名核对 +- 后端报「插入数据失败(第N行):查询字段不存在/字段名称不正确」时,多半是 **allData 缺了列名表头行**(后端把首行数据当成了字段名),或某行列数与 `columns` 对不上——确认首行是列名、每行值个数 = 列总数 --- @@ -798,28 +860,55 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 - `database`:数据库级别权限 - `table`:表级别权限 -### 7.7 撤销权限 +> ⚠️ 权限为「仅追加」模型:`grant_api_key_permissions` 只能新增权限,不会覆盖已有权限,且后端**不支持撤销/删除已授予的权限**。授权前务必确认范围,授错只能删掉整个密钥(`delete_api_key`)后重建。 + +### 7.7 调整权限(只能重建密钥) ``` -调用: get_api_key_permissions(apiKeyId="7") -返回: { - "data": { - "connectionPermissions": [{"id": "101", "connectionId": "58", "permissionType": "read"}], - "databasePermissions": [...], - "tablePermissions": [...] - } -} - -调用: revoke_api_key_permissions(permissionIds=["101"]) +# 后端不支持撤销单条权限。如需收回某密钥的权限,只能删除密钥后重新创建并重新授权: +调用: delete_api_key(id="7") +调用: create_api_key(apiKeyName="xxx") +调用: grant_api_key_permissions(apiKeyId="<新密钥ID>", batchDatas=[...]) ``` -> 说明:`revoke_api_key_permissions` 按权限记录的 `id` 删除,需先从 `get_api_key_permissions` 获取。 +> 说明:后端 permission 删除接口返回「不支持当前的调用方式」,无法撤销单条已授予权限。要缩小权限范围,走「删密钥 → 重建 → 重新授权」。 --- ## 场景 8:技能与工具管理 -当用户需要创建和管理自定义技能时,使用此流程。 +当用户需要把 SQL 沉淀为数据源的可复用工具时,使用此流程。 + +> 🔒 **核心约束:技能(skill)必须挂着工具才有效**。后端/前端都没有「只建空技能」这个动作——单独建技能会留下一个无效的空技能。前端唯一入口是「添加工具」,它会按需把技能建好、配好,最后必定以创建工具收尾。 + +### 8.0 一步到位:把 SQL 沉淀为工具(推荐) + +**优先用 `add_sql_tool_to_datasource`**,它一步完成整条链路,保证技能必有工具: + +``` +调用: add_sql_tool_to_datasource( + datasourceId="58", + name="查询活跃用户", + businessDescription="查询所有状态为活跃的用户", + sqlTemplate="SELECT * FROM users WHERE status = #{status}", + sqlParams={"type":"object","required":["status"],"properties":{"status":{"type":"string","description":"用户状态","examples":["active"]}}}, // 可选 + resultType="list", // 可选,默认 list + businessScenario="用于查看当前活跃用户列表", // 可选 + tableIds=["5"] // 可选 +) +``` + +> **sqlParams 格式**:标准 JSON Schema,形如 `{"type":"object","required":[...],"properties":{"参数名":{"type":"...","description":"...","examples":[...]}}}`。`properties` 的键要与 SQL 模板里的 `#{参数名}` 占位符一一对应。传 dict 会自动序列化为 JSON 字符串;无参数可传 `{}`。 + +内部流程(= 前端 handleAddToolSubmit): +``` +读 skillBool(GET /datasource/config/{id}) +├─ 技能已存在 → getByDatasource 拿 skillId → 按 sqlTemplate 去重 → confirmTools 建工具 +└─ 技能不存在 → createOrGet 建技能 → getByDatasource 拿 skillId → 去重 + → updateOrGet 写 lzwcai-mcp-sqlexecutor 配置模板 → confirmTools 建工具 +``` +- **幂等**:同 datasourceId 下若已有 `sqlTemplate` 相同的工具(空白归一化后比较),返回 `skipped`,不重复创建。 +- **skillId 来自 getByDatasource**,不是 createOrGet 的返回(后端 createOrGet 不回可靠 id)。 ### 8.1 查看数据源关联的技能 @@ -827,21 +916,23 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 调用: get_skill_by_datasource(datasourceId="58") ``` -### 8.2 创建技能 - -``` -调用: create_skill(datasourceId="58", name="订单查询技能", description="用于订单数据的常用查询") -``` - ### 8.3 查看技能下的工具 ``` 调用: get_skill_tools(skillId="xx") ``` -### 8.4 将 SQL 创建为可复用工具 +--- + +### 分步操作(高级,一般不需要) + +> ⚠️ **不要单独建空技能**:技能必须挂着工具才有效,平时不会单独创建技能。常规「把 SQL 沉淀为工具」一律用 `add_sql_tool_to_datasource`(它会按需建技能+配模板+建工具)。下面的散工具仅用于「技能已存在」时单独加/改工具。 + +### 8.4 向已有技能添加 SQL 工具(底层积木,需技能已存在) ``` +# 前提:技能已存在(用 get_skill_by_datasource 拿到 skillId); +# 若技能不存在,请直接用 add_sql_tool_to_datasource,别手动建空技能 调用: create_sql_tool( skillId="xx", tableIds=["5"], @@ -849,7 +940,7 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 "name": "查询活跃用户", "businessDescription": "查询所有状态为活跃的用户", "sqlTemplate": "SELECT * FROM users WHERE status = #{status}", - "sqlParams": {"status": {"type": "string", "default": "active"}}, + "sqlParams": {"type":"object","required":["status"],"properties":{"status":{"type":"string","description":"用户状态","examples":["active"]}}}, "resultType": "list", "businessScenario": "用于查看当前活跃用户列表" }] @@ -870,12 +961,24 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 ### 8.6 更新技能配置 ``` +# datasourceId + skillId 均必填(真实 ID,来自其他工具返回,不可臆造) +# 只给这两个 ID 即自动生成 lzwcai-mcp-sqlexecutor 标准配置模板 +调用: update_skill_config( + datasourceId="58", // 来自 list_databases / list_tables_with_ai / get_connection_config_list + skillId="xx" // 来自 get_skill_by_datasource 返回 +) + +# 或:手动指定完整 configTemplate(覆盖自动生成) 调用: update_skill_config( datasourceId="58", - configTemplate='{"mcpServer": "..."}' // JSON 字符串 + skillId="xx", + configTemplate='{"mcpServers": {...}}' // JSON 字符串 ) ``` +> 说明:configTemplate 大部分是固定值,只有 mcpServerKey 后缀、`env.databaseId`、`env.skillId` 随 datasourceId / skillId 变化。两个 ID 都必填且必须是其他工具返回的真实值。不显式传 configTemplate 时,工具会按标准模板自动生成(与前端 SqlControllerMsg.vue 一致),无需手写整段 JSON。 + + ### 8.7 修改技能下某个工具 ``` diff --git a/.kilo/skills/数字员工平台数据库技能的.md b/.kilo/skills/数字员工平台数据库技能的.md new file mode 100644 index 0000000..febc094 --- /dev/null +++ b/.kilo/skills/数字员工平台数据库技能的.md @@ -0,0 +1,343 @@ +# 数字员工平台数据库技能配置指南 + +## 概述 + +本文档介绍两种使用数字员工平台数据库技能的方法: +- **方法一**:通过数字员工对话直接使用 +- **方法二**:通过 AI 编辑器的 Skills + MCP 搭配使用 + +--- + +## 方法一:通过数字员工对话使用 + +### 配置说明 + +此方法通过配置 MCP Server 来连接数字员工平台。 + +### MCP 配置示例 + +```json +{ + "mcpServers": { + "lzwcai-mcp-agile-db": { + "command": "uvx", + "type": "stdio", + "args": [ + "lzwcai-mcp-agile-db" + ], + "timeout": 600, + "env": { + "API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q" + } + } + } +} +``` + +### 注意事项 + +- 配置中的 `API_KEY` 是数字员工平台的密钥 +- 存在单点登录问题,请使用不会过期的账号密钥 +- Skills 配置参考:`lzwcai-agile-db` + +--- + +## 方法二:通过 AI 编辑器使用 + +### 配置说明 + +此方法通过下载 Skills 和 MCP 配置,在 AI 编辑器中搭配使用,支持直接对话操作。 + +### MCP 配置示例 + +```json +{ + "mcpServers": { + "lzwcai_mcp_agile_db": { + "command": "uvx", + "args": [ + "lzwcai-mcp-agile-db" + ], + "env": { + "UV_INDEX_URL": "http://192.168.2.236:3141/lzwc/dev/+simple/", + "API_KEY": "eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjljMDllMjZhLWFkNzgtNGNmMi05YzQ5LWQzY2Y5NjI5MGRjYyJ9.v5ZffkvM2CnMkoWqc-Xy-0gN2oLBjyfJlm_YnCE6TWBwfKVmpuakoRXFMTiHQG8LOphv7JlEmZHcYpzwM52D0Q", + "backendBaseUrl": "http://192.168.2.236:8088" + } + } + } +} +``` + +### 环境变量说明 + +| 变量名 | 说明 | 示例值 | +|--------|------|--------| +| `UV_INDEX_URL` | Python 包索引地址 | `http://192.168.2.236:3141/lzwc/dev/+simple/` | +| `API_KEY` | 数字员工平台 API 密钥 | (见配置示例) | +| `backendBaseUrl` | 后端服务基础地址 | `http://192.168.2.236:8088` | + +--- + +## 使用建议 + +1. **选择方法**:根据实际使用场景选择合适的方法 + - 数字员工对话:适合快速使用 + - AI 编辑器:适合更灵活的配置和使用 + +2. **密钥管理**:妥善保管 `API_KEY`,避免泄露 + +3. **网络配置**:确保网络可以访问配置的后端地址 + +--- + +## 相关资源 + +- Skills 目录:`lzwcai-agile-db/` +- 详细技能文档:`lzwcai-agile-db/SKILL.md` + +--- + +## 对话使用场景示例 + +以下是配置好技能后,您可以直接使用的对话示例: + +### 📊 场景 1:浏览数据源和表 + +**用户:** "帮我看看有哪些数据源" + +**AI 回复:** +``` +共找到 3 个数据源: +1. HMD产品(PostgreSQL, 运行中) +2. 测试数据库(MySQL, 运行中) +3. 历史数据(Oracle, 已停止) + +请告诉我您想看哪个数据源? +``` + +--- + +**用户:** "看看HMD产品有哪些表" + +**AI 回复:** +``` +HMD产品 数据源包含以下数据库和表: + +order_db: + - orders (订单表, 15 个字段, 2568 条记录) + - users (用户表, 8 个字段, 156 条记录) + - products (商品表, 12 个字段, 892 条记录) + +analytics_db: + - daily_stats (日报表, 10 个字段, 365 条记录) +``` + +--- + +### 🔍 场景 2:查询表数据 + +**用户:** "查一下users表前10条数据" + +**AI 回复:** +``` +users 表共 156 条记录,当前显示第 1-10 条: + +| id | username | email | status | created_at | +|----|----------|-----------------|--------|---------------------| +| 1 | admin | admin@test.com | active | 2024-01-01 00:00:00 | +| 2 | user1 | user1@test.com | active | 2024-01-02 10:30:00 | +| 3 | user2 | user2@test.com | inactive| 2024-01-03 15:45:00 | +... + +注:默认查询测试环境,如需查询生产环境请说明。 +``` + +--- + +### 📝 场景 3:执行 SQL 查询 + +**用户:** "统计每个地区的订单数量" + +**AI 回复:** +``` +各地区订单统计: + +| 地区 | 订单数 | +|------|--------| +| 华东 | 1,250 | +| 华南 | 980 | +| 华北 | 756 | +| 西南 | 432 | +| 东北 | 289 | +``` + +--- + +### ➕ 场景 4:插入数据 + +**用户:** "新增一个用户,用户名是test_user,邮箱test@test.com" + +**AI 回复:** +``` +即将向 users 表插入以下数据: + username = test_user + email = test@test.com + (id 自动生成) + +确认插入吗? +``` + +**用户:** "确认" + +**AI 回复:** +``` +✅ 已成功插入用户 test_user (ID: 157) +``` + +--- + +### ✏️ 场景 5:更新数据 + +**用户:** "把ID为5的用户邮箱改成new@test.com" + +**AI 回复:** +``` +即将更新 users 表 ID=5 的记录: + email: old@test.com → new@test.com + +确认更新吗? +``` + +**用户:** "确认" + +**AI 回复:** +``` +✅ 已更新用户 ID=5 的邮箱为 new@test.com +``` + +--- + +### 🗑️ 场景 6:删除数据 + +**用户:** "删除ID为10的订单" + +**AI 回复:** +``` +⚠️ 安全提醒:此操作存在删除风险。 +具体说明:数据将永久删除,不可恢复。 + +确认要删除以下记录吗? + 订单 ID=10, 订单号=ORD-2024-0010, 金额=¥1,250.00 + +请回复"确认删除"继续。 +``` + +**用户:** "确认删除" + +**AI 回复:** +``` +✅ 已成功删除订单 ID=10 +``` + +--- + +### 🤖 场景 7:AI 生成表结构 + +**用户:** "帮我设计一个商品表,包含名称、价格、库存、分类" + +**AI 回复:** +``` +AI 已生成表结构设计: + +表名: products (商品表) +字段: + - id (SERIAL, 主键) - 主键ID + - name (VARCHAR(200), 非空) - 商品名称 + - price (DECIMAL(10,2)) - 价格 + - stock (INTEGER) - 库存数量 + - category (VARCHAR(50)) - 分类 + - created_at (TIMESTAMP) - 创建时间 + +是否需要在此基础上修改或直接创建? +``` + +**用户:** "直接创建到order_db数据库" + +**AI 回复:** +``` +✅ 已成功创建表 products (商品表) +``` + +--- + +### 📥 场景 8:导入 Excel 数据 + +**用户:** "帮我导入这个Excel文件:https://example.com/data.xlsx" + +**AI 回复:** +``` +AI 已识别 Excel 文件内容: + +表名: customers (客户表) +字段: + - id (SERIAL, 主键) + - name (VARCHAR(100)) - 客户名称 + - phone (VARCHAR(20)) - 电话 + - address (TEXT) - 地址 + +预览数据(前3条): +| id | name | phone | address | +|----|--------|------------|----------------| +| 1 | 张三 | 13800138000| 北京市朝阳区 | +| 2 | 李四 | 13900139000| 上海市浦东新区 | +| 3 | 王五 | 13700137000| 广州市天河区 | + +共 50 条数据待导入。 + +⚠️ 安全提醒:此操作可能涉及数据安全风险。 +说明:导入的数据将写入数据库,请确认数据来源合法合规。 +请确认是否继续? +``` + +**用户:** "继续" + +**AI 回复:** +``` +✅ 成功导入 50 条数据到 customers 表 +``` + +--- + +### 🔑 场景 9:API 密钥管理 + +**用户:** "帮我创建一个新的API密钥" + +**AI 回复:** +``` +请输入密钥名称: +``` + +**用户:** "测试密钥" + +**AI 回复:** +``` +✅ 已创建 API 密钥: +名称: 测试密钥 +密钥: lzwc_sk_xxxxxxxxxxxx +状态: 启用 +过期时间: 2025-06-18 + +请妥善保管密钥,丢失后无法找回。 +``` + +--- + +## ⚠️ 重要使用提示 + +1. **环境选择:** 默认使用测试环境(`test`),操作生产环境需要明确说明"查询生产环境"或"操作生产环境" + +2. **安全确认:** 所有写操作(增删改)都会先预览并等待您的确认 + +3. **多选原则:** 有多个数据源/数据库/表可选时,AI会列出让您选择,不会擅自做主 + +4. **分步执行:** 复杂任务会分步完成,每一步都会确认后再继续 diff --git a/lzwcai_mcp_agile_db/README.md b/lzwcai_mcp_agile_db/README.md index c996cd7..7c6f4c0 100644 --- a/lzwcai_mcp_agile_db/README.md +++ b/lzwcai_mcp_agile_db/README.md @@ -57,16 +57,16 @@ lzwcai-mcp-agile-db - `toggle_api_key_status` - 启用/禁用密钥 - `delete_api_key` - 删除密钥 - `get_api_key_permissions` - 查看密钥权限 -- `grant_api_key_permissions` - 授予权限 -- `revoke_api_key_permissions` - 撤销/删除已授予权限 +- `grant_api_key_permissions` - 授予权限(仅追加,不可撤销) ### 技能与工具管理 +- `add_sql_tool_to_datasource` - 把 SQL 沉淀为工具(一步到位,自动建技能+配模板+建工具,推荐入口) - `get_skill_by_datasource` - 获取技能信息 - `get_skill_tools` - 获取技能工具列表 -- `create_skill` - 创建技能 -- `create_sql_tool` - 创建 SQL 工具 +- `create_sql_tool` - 创建 SQL 工具(需技能已存在) - `delete_skill_tool` - 删除技能工具 - `update_skill_config` - 更新技能配置 +- `update_skill_tool` - 修改技能工具 ### 数据导入 - `preview_import_data` - 预览导入数据 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 5294a1babc5866cf3074601a2ef7678ea711bbb8..e83a7aaddb0799e676bdf9c67a13009cffea9edd 100644 GIT binary patch delta 40 vcmZoMY&PUQ&CAQh00duCjkBI^%m#%iEvsAn%m&Ss!zsAp(2`MJO!M(xc{1n08>0PHUef&c&j 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 f62c1f9..5ab3e30 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,10 +1,377 @@ -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"} +2026-06-22 23:02:48 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs +2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db' +2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 23:02:48 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 23:02:48 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:73] - [客户端初始化] base_url=https://dempdemo.lzwcai.com/api, 认证方式=account:demp04 +2026-06-22 23:02:48 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:118] - [登录] POST https://dempdemo.lzwcai.com/api/login, username=demp04, loginType=user +2026-06-22 23:02:48 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None +2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value= +2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.started ssl_context= server_hostname='dempdemo.lzwcai.com' timeout=30.0 +2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.complete return_value= +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Mon, 22 Jun 2026 15:02:58 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')]) +2026-06-22 23:02:49 - httpx - INFO - [_client.py:1740] - HTTP Request: POST https://dempdemo.lzwcai.com/api/login "HTTP/1.1 200 " +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:161] - [API响应] HTTP 200 +2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:137] - [登录] 成功获取 token +2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.started +2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete +2026-06-23 09:47:20 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs +2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db' +2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ================================================== +2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动 +2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56 +2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ================================================== +2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式) +2026-06-23 09:47:20 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') +2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-23 09:47:21 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求 +2026-06-23 09:47:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8088, 认证方式=account:yy8z9 +2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具 +2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-23 09:47:28 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys +2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8088/login, username=yy8z9, loginType=user +2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None +2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value= +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')]) +2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8088/login "HTTP/1.1 200 " +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200 +2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token +2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8088/datasource/api_key/list +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')]) +2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8088/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 " +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200 +2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys +2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: { + "total": 21, + "rows": [ + { + "createBy": "", + "createTime": "2026-06-18 11:29:46", + "updateBy": "", + "updateTime": "2026-06-18 11:59:25", + "remark": null, + "id": "37", + "apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY", + "apiKeyName": "盒马超市只读访问密钥", + "enterpriseId": "1932095424144715777", + "status": 0, + "expireTime": "2027-06-18T11:59:25.000+08:00" + }, + { + "createBy": "", + "createTime": "2026-06-18 10:54:40", + ... +2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.started +2026-06-23 09:47:33 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete +2026-06-23 09:48:33 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs +2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db' +2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ================================================== +2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动 +2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56 +2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ================================================== +2026-06-23 09:48:33 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式) +2026-06-23 09:48:33 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-23 09:48:33 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') +2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-23 09:48:34 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求 +2026-06-23 09:48:34 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9 +2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具 +2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-23 09:48:36 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys +2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user +2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None +2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value= +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')]) +2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 " +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200 +2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token +2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')]) +2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 " +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200 +2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys +2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: { + "total": 21, + "rows": [ + { + "createBy": "", + "createTime": "2026-06-18 11:29:46", + "updateBy": "", + "updateTime": "2026-06-18 11:59:25", + "remark": null, + "id": "37", + "apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY", + "apiKeyName": "盒马超市只读访问密钥", + "enterpriseId": "1932095424144715777", + "status": 0, + "expireTime": "2027-06-18T11:59:25.000+08:00" + }, + { + "createBy": "", + "createTime": "2026-06-18 10:54:40", + ... +2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.started +2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete +2026-06-23 11:11:10 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs +2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db' +2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ================================================== +2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动 +2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56 +2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ================================================== +2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式) +2026-06-23 11:11:10 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') +2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-23 11:11:11 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求 +2026-06-23 11:11:11 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9 +2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具 +2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-23 11:11:14 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-23 11:11:14 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys +2026-06-23 11:11:14 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user +2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None +2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value= +2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')]) +2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 " +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200 +2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token +2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')]) +2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 " +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200 +2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys +2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: { + "total": 21, + "rows": [ + { + "createBy": "", + "createTime": "2026-06-18 11:29:46", + "updateBy": "", + "updateTime": "2026-06-18 11:59:25", + "remark": null, + "id": "37", + "apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY", + "apiKeyName": "盒马超市只读访问密钥", + "enterpriseId": "1932095424144715777", + "status": 0, + "expireTime": "2027-06-18T11:59:25.000+08:00" + }, + { + "createBy": "", + "createTime": "2026-06-18 10:54:40", + ... +2026-06-23 11:11:15 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-23 11:11:27 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys +2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list +2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.started +2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete +2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None +2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=utf-8'), (b'Content-Length', b'51'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN')]) +2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 " +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:239] - [认证] 收到 401(登录过期),尝试重新登录后重试一次 +2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')]) +2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 " +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200 +2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')]) +2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 " +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request= +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started +2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete +2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200 +2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys +2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: { + "total": 21, + "rows": [ + { + "createBy": "", + "createTime": "2026-06-18 11:29:46", + "updateBy": "", + "updateTime": "2026-06-18 11:59:25", + "remark": null, + "id": "37", + "apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY", + "apiKeyName": "盒马超市只读访问密钥", + "enterpriseId": "1932095424144715777", + "status": 0, + "expireTime": "2027-06-18T11:59:25.000+08:00" + }, + { + "createBy": "", + "createTime": "2026-06-18 10:54:40", + ... +2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.started +2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete +2026-06-23 11:37:08 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs +2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db' +2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-23 11:37:08 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401(登录过期),尝试重新登录后重试一次 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200 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 eb8d141..2371c16 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,2 +1 @@ -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"} +2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401 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 4f144cf..c3da3d0 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/server.py @@ -111,7 +111,7 @@ async def run_server(): streams[1], InitializationOptions( server_name="lzwcai_mcp_agile_db", - server_version="0.1.12", + server_version="0.1.8", capabilities=server.get_capabilities( notification_options=NotificationOptions(), experimental_capabilities={}, 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 db7da021f473f035e0ade09c0e028b32e4a86830..24bde3f69eec4bdfee031386a8bb1515a127e40d 100644 GIT binary patch delta 761 zcmZuu&rcIU6y_~pNee}O_yahw#6$%xBxs@`U`*7+L_&z5(WIu-GQovHH@g&ViJ=IX z77InEHleKu3WneTbWK1k@&|DBfSic!Oxt)UoHU-C1?!2)ye~8Fd+*Jg_ibFI8u4kej9KILMQV}ni7>~bSjIE4O z|04Ac(igKyzpIA0r<0SjTC)6+j=N*4?s#xGo#Bp$U(@hva>1Ke9OOkLbqlgA*n0T= z96CNnd~Xx$eiE8edr222b!(V-##o`m(ooz#PXnJx#K$^hzhQ0Amm_MZZzA?J#7n5p z)`fVoJV!>Bqpl~)CPcXb@mihH@xWjfjG|=gM3!Eo)%nn377Vh}(UCrPTSSA5q&_4W zM6;#K+=2cvCdQb|9fB2^+V3Ce>$Taeve7IGrY_`=t3}6_20cO+doOYxYZPRh$|{Bu zoRv*Mr~J(3IXHuJb4*aKSaYs$P>+|i%}}F&?l(Xq{*#N~veK6K2{>mtp2;o7Rr#%8 z!~^+vp;`H!e-;ikGMVjsV-;$T?>0nYE|JD?klW?J9t!IiW@WtpDvQ2ABT%>lsBog3T@2fAcNtIZQV)N z)g3OQ=q86Vi?K$fn<}+AcGye&3l+p%Q~&?~ delta 1665 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 E1J1e@+5i9m delta 1617 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�X7 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 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/data_import.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/data_import.py index 7ea63f3..5ec5041 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 @@ -125,19 +125,44 @@ class PreviewImportDataTool(ToolDef): class ConfirmImportDataTool(ToolDef): name = "confirm_import_data" description = ( - "确认导入 AI 识别后的数据(建表+插数据)。" - "传入 preview_import_data 返回的 data 原文 + databaseName 即可," - "工具内部会自动组装成后端要求的 {tableStructure(含databaseName), allData} 结构" + "确认导入 AI 识别后的数据(建表+插数据),第二步。第一步先调 preview_import_data。\n" + "【data 传什么】把 preview_import_data 返回的 data 原文整块传给 data 参数即可," + "工具会自动解包并组装成后端要求的 {tableStructure(单表对象,含databaseName), allData} 结构。\n" + "data 的标准形态(= preview 的返回):\n" + " {\n" + " \"tableStructure\": { \"success\":true, \"message\":\"...\",\n" + " \"data\": { \"tables\": [ { \"tableName\":\"animals\", \"columns\":[...] } ] },\n" + " \"allData\": [ [列名表头行...], [行1各列值...], [行2各列值...] ] },\n" + " \"databaseName\": \"目标库名\", \"target\": \"prod|test\"\n" + " }\n" + "【allData 的结构(关键)】allData 是二维数组:\n" + " · 首行 allData[0] 是【表头行】= 各列的 columnName(列名),真实数据从 allData[1] 起;\n" + " · 每行(含表头行)都是「按 columns 顺序排列的位置数组」,行宽 = 列总数(含 SERIAL 主键等所有列,不裁剪);\n" + " · 若调用方传的 allData 没带表头行(首行就是数据),工具会据列名自动补一行表头——" + "否则后端会把首行数据当成字段名,报「查询字段不存在/字段名称不正确」。\n" + "【列名必须对应目标表真实字段】tableStructure.columns(及 allData 首行表头)里的列名," + "必须是目标表中确实存在的字段名(后端按列名拼 INSERT)。导入到已有表时," + "不要直接用 Excel 识别出的列,应先调 get_table_detail 拿到目标表真实字段," + "再把 columns、表头、各行取值对齐到这些真实字段,否则报「查询字段不存在/字段名称不正确」。\n" + "databaseName/target 既可放顶层参数,也可放在 data 里,工具都能识别。" ) input_schema = { "type": "object", "properties": { - "connectionId": {"type": "string", "description": "数据源连接 ID"}, - "databaseName": {"type": "string", "description": "落库的数据库名(导入目标库)"}, - "target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"}, - "data": {"type": "object", "description": "preview_import_data 返回的 data(含 tableStructure/allData),或已组装好的最终结构"}, + "connectionId": {"type": "string", "description": "数据源连接 ID(同 preview 用的那个)"}, + "databaseName": {"type": "string", "description": "落库的数据库名(导入目标库)。必填——顶层不给会尝试从 data.databaseName 回捞"}, + "target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test。可放顶层或 data 里"}, + "data": { + "type": "object", + "description": ( + "preview_import_data 返回的 data 原文整块(含 tableStructure{success,message,data:{tables:[...]}} 与 allData)。" + "allData 为二维数组:首行是列名表头(allData[0])、数据行从 allData[1] 起," + "每行按 columns 顺序给出全部列的值,行宽 = 列总数(不裁剪自增列)。" + "缺表头时工具会据列名自动补。也接受调用方已组装好的最终结构。" + ), + }, }, - "required": ["connectionId", "data"], + "required": ["connectionId", "databaseName", "data"], } @staticmethod @@ -160,14 +185,15 @@ class ConfirmImportDataTool(ToolDef): ts = data.get("tableStructure") single_table = None + ts_inner = {} if isinstance(ts, dict): if "columns" in ts: # 已是单表对象(调用方自行组装过) single_table = dict(ts) else: # preview 包装:tableStructure.data.tables[0] - inner = ts.get("data") if isinstance(ts.get("data"), dict) else {} - tables = inner.get("tables") if isinstance(inner, dict) else None + ts_inner = ts.get("data") if isinstance(ts.get("data"), dict) else {} + tables = ts_inner.get("tables") if isinstance(ts_inner, dict) else None if isinstance(tables, list) and tables: single_table = dict(tables[0]) @@ -178,19 +204,119 @@ class ConfirmImportDataTool(ToolDef): if database_name and not single_table.get("databaseName"): single_table["databaseName"] = database_name - all_data = data.get("allData") + # allData 可能落在多个层级(取决于调用方/preview 的嵌套方式),按优先级查找: + # 1. data.allData —— 与 tableStructure 平级(约定的标准位置) + # 2. tableStructure.allData —— 嵌在 tableStructure 包装内(真机/AI 常见误放) + # 3. tableStructure.data.allData —— 嵌在内层 data 里 + # 注意:只接受 list;任何非 list(如内层 data 的 tables 包装对象)都视为未命中, + # 避免把 dict 误当成行数据传给后端。 + all_data = None + for candidate in ( + data.get("allData"), + ts.get("allData") if isinstance(ts, dict) else None, + ts_inner.get("allData") if isinstance(ts_inner, dict) else None, + ): + if isinstance(candidate, list): + all_data = candidate + break if all_data is None: - all_data = data.get("data") or [] + all_data = [] + + # 表头行:后端约定 allData[0] 是「表头行」(列名数组),真实数据从 allData[1] 起 + # (见前端 TableRecognition.vue handleComplete 与 CustomizeDbTable.vue validateDataColumns)。 + # 若调用方传的 allData 没带表头(首行就是数据),后端会把首行数据当成字段名, + # 报「查询字段不存在/字段名称不正确」。这里据列名补出表头行。 + all_data = ConfirmImportDataTool._ensure_header(single_table.get("columns"), all_data) + return {"tableStructure": single_table, "allData": all_data} + @staticmethod + def _column_names(columns): + """从列定义中按顺序提取列名数组。""" + if not isinstance(columns, list): + return [] + return [c.get("columnName") for c in columns if isinstance(c, dict) and c.get("columnName")] + + @staticmethod + def _ensure_header(columns, all_data): + """确保 allData[0] 是「表头行」(列名数组)。 + + 后端约定:allData[0] 为表头(列名),真实数据行从 allData[1] 起;数据行按 columns + 顺序给出【全部列】的值(不裁剪自增列)。前端 TableRecognition.vue 在提交前总会把 + 列名作为首行 push 进 allData。若调用方(含 AI)传来的 allData 首行已经是数据(缺表头), + 后端会把首行当列名解析,报「查询字段不存在/字段名称不正确」。这里据列名补表头: + - 首行恰好等于列名数组 → 视为已带表头,原样返回 + - 否则 → 在最前面补一行列名 + """ + names = ConfirmImportDataTool._column_names(columns) + if not names or not isinstance(all_data, list) or not all_data: + return all_data + first = all_data[0] + if isinstance(first, list) and list(first) == names: + return all_data # 已带表头 + return [names, *all_data] + async def execute(self, args: dict) -> dict: args = dict(args) connection_id = args.pop("connectionId") - target = args.pop("target", "test") + target = args.pop("target", None) database_name = args.pop("databaseName", None) data = args.pop("data") + # 容错:databaseName / target 可能被放进 data 里(AI 常把 preview 返回的整块连同 + # databaseName/target 一起塞进 data)。顶层没给时,从 data 里回捞,并清出 data + # 避免污染最终 body。 + if isinstance(data, dict): + if database_name is None and data.get("databaseName"): + database_name = data.get("databaseName") + if target is None and data.get("target"): + target = data.get("target") + data = {k: v for k, v in data.items() if k not in ("databaseName", "target")} + + if target is None: + target = "test" + body = self._build_body(data, database_name) + + # 预检:把后端那两个含糊的报错(「导入数据不能为空」/「数据库名称不能为空」) + # 提前在工具层拦下,给出可操作的提示(指明 allData/databaseName 该放哪), + # 避免调用方对着后端原文反复试错。仅在 body 已被识别为标准结构时校验。 + if isinstance(body, dict) and "tableStructure" in body: + if not body.get("allData"): + raise ValueError( + "导入数据为空:未能从 data 中解析到 allData(数据行)。" + "请确认 allData 是一个非空数组,可放在 data.allData、" + "data.tableStructure.allData 或 data.tableStructure.data.allData 任一层级。" + ) + ts = body["tableStructure"] + if isinstance(ts, dict) and not ts.get("databaseName"): + raise ValueError( + "缺少 databaseName(落库目标库名):请通过顶层参数 databaseName 传入," + "或放在 data.databaseName 中(工具会自动塞进表对象)。" + ) + + # 行宽与表头校验:_ensure_header 已保证 allData[0] 是表头行(列名)。 + # 后端要求每行(含表头)宽度 = 列数(全部列,含自增列占位),且表头之外至少有 1 行数据。 + # 行宽对不上后端只会回含糊的「字段名称不正确/查询字段不存在」,这里提前报清楚。 + cols = ts.get("columns") if isinstance(ts, dict) else None + all_data = body["allData"] + if isinstance(cols, list) and cols: + total = len(cols) + # 表头之外至少要有一行真实数据 + data_rows = [r for r in all_data[1:] if isinstance(r, list)] + if not data_rows: + raise ValueError( + "导入数据为空:allData 除表头行外没有任何数据行。" + "allData 约定首行为表头(列名),真实数据从第 2 行起。" + ) + for idx, row in enumerate(all_data): + if isinstance(row, list) and len(row) != total: + raise ValueError( + f"第 {idx + 1} 行列数为 {len(row)},与表结构的 {total} 列不匹配。" + "allData 每行(含表头行)都应按 columns 顺序给出全部列的值;" + "首行须为列名表头,数据行从第 2 行起。请核对是否多/少了列。" + ) + return await self.client.post( f"/datasource/connection/{connection_id}/import_document/confirm", json_data=body, 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 590d7eb..5674fa0 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 @@ -1,5 +1,9 @@ """ -技能与工具管理工具 (工具 24-29) +技能与工具管理工具 + +把 SQL 沉淀为数据源可复用工具的入口统一走 add_sql_tool_to_datasource(保证技能必有工具, +内部按需建技能+写配置+建工具)。其余为底层积木:查询类、向已有技能加/改工具、改技能配置。 +不单独暴露「只建技能」工具,避免产生无效空技能。 """ import json @@ -39,24 +43,6 @@ class GetSkillToolsTool(ToolDef): return await self.client.get(f"/datasource/skill/getBySkillId/{args['skillId']}") -@register_tool("create_skill") -class CreateSkillTool(ToolDef): - name = "create_skill" - description = "为数据源创建技能" - input_schema = { - "type": "object", - "properties": { - "datasourceId": {"type": "string", "description": "数据源 ID"}, - "name": {"type": "string", "description": "技能名称(不传则自动生成)"}, - "description": {"type": "string", "description": "技能描述"}, - }, - "required": ["datasourceId"], - } - - async def execute(self, args: dict) -> dict: - return await self.client.post("/datasource/skill/createOrGet", json_data=args) - - @register_tool("create_sql_tool") class CreateSqlToolTool(ToolDef): name = "create_sql_tool" @@ -79,7 +65,7 @@ class CreateSqlToolTool(ToolDef): "name": {"type": "string", "description": "工具名称"}, "businessDescription": {"type": "string", "description": "业务描述"}, "sqlTemplate": {"type": "string", "description": "SQL 模板(支持 #{param} 参数占位)"}, - "sqlParams": {"type": "string", "description": "参数 JSON Schema(JSON 字符串或对象)"}, + "sqlParams": {"type": "string", "description": "参数定义,最终以 JSON 字符串提交(传 dict 会自动 json.dumps)。内容形态后端不挑剔:可为 JSON Schema 对象串如 {\"type\":\"object\",...},也可为字段定义数组串;无参数传 \"{}\""}, "resultType": {"type": "string", "enum": ["single", "list"], "default": "list", "description": "结果类型,默认 list"}, "businessScenario": {"type": "string", "description": "业务场景描述"}, }, @@ -92,11 +78,17 @@ class CreateSqlToolTool(ToolDef): async def execute(self, args: dict) -> dict: args = dict(args) - # 处理 suggestions 中的 sqlParams + # tableIds 后端始终期望该键存在:前端真机始终传 ""(空串)。 + # 调用方未给时补 "",与前端 postSqlSkillConfirmTools 的请求保持一致。 + if "tableIds" not in args or args["tableIds"] is None: + args["tableIds"] = "" + # 处理 suggestions 中的 sqlParams:dict 自动序列化为 JSON 字符串; + # 同时补齐 resultType 默认值 list(与前端默认一致)。 if "suggestions" in args and isinstance(args["suggestions"], list): for suggestion in args["suggestions"]: if "sqlParams" in suggestion and isinstance(suggestion["sqlParams"], dict): suggestion["sqlParams"] = json.dumps(suggestion["sqlParams"]) + suggestion.setdefault("resultType", "list") return await self.client.post("/datasource/skill/confirmTools", json_data=args) @@ -119,38 +111,85 @@ class DeleteSkillToolTool(ToolDef): @register_tool("update_skill_config") class UpdateSkillConfigTool(ToolDef): name = "update_skill_config" - description = "更新技能(名称/描述/MCP 配置模板等)。对应后端 skill/updateOrGet" + description = ( + "更新技能(名称/描述/MCP 配置模板等)。对应后端 skill/updateOrGet。" + "datasourceId 与 skillId 均必填且为真实 ID(来自其他工具返回,不可臆造)。" + "若不显式传 configTemplate,会按这两个 ID 自动生成 lzwcai-mcp-sqlexecutor 的标准 MCP 配置模板(与前端一致)。" + ) input_schema = { "type": "object", "properties": { - "datasourceId": {"type": "string", "description": "数据源 ID"}, - "skillId": {"type": "string", "description": "技能 ID(可选)"}, + "datasourceId": { + "type": "string", + "description": "数据源/配置 ID(真实 ID,来自 list_databases / list_tables_with_ai / get_connection_config_list,不可臆造)", + }, + "skillId": { + "type": "string", + "description": "技能 ID(真实 ID,来自 get_skill_by_datasource 的返回,不可臆造;与 datasourceId 一起用于生成 configTemplate)", + }, "name": {"type": "string", "description": "技能名称(可选)"}, "description": {"type": "string", "description": "技能描述(可选)"}, - "configTemplate": {"type": "string", "description": "配置模板 JSON 字符串(可选)"}, + "configTemplate": { + "type": "string", + "description": "配置模板 JSON 字符串(可选)。不传时按 datasourceId + skillId 自动生成标准模板", + }, }, - "required": ["datasourceId"], + "required": ["datasourceId", "skillId"], } + @staticmethod + def _build_config_template(datasource_id: str, skill_id: str) -> str: + """生成 lzwcai-mcp-sqlexecutor 的标准 MCP 配置模板。 + + 模板大部分是固定值,仅 mcpServerKey 后缀、env.databaseId、env.skillId 随 + datasourceId / skillId 动态变化(与前端 SqlControllerMsg.vue 的 configTemplateObj 完全一致)。 + """ + mcp_server_key = f"lzwcai_mcp_sqlexecutor_{datasource_id}" + config_obj = { + "mcpServers": { + mcp_server_key: { + "command": "uvx", + "type": "stdio", + "args": ["lzwcai-mcp-sqlexecutor"], + "tiemout": 200, + "env": { + "databaseId": datasource_id, + "skillId": skill_id, + }, + } + } + } + return json.dumps(config_obj) + async def execute(self, args: dict) -> dict: args = dict(args) # 如果 configTemplate 是 dict,转为 JSON 字符串 if "configTemplate" in args and isinstance(args["configTemplate"], dict): args["configTemplate"] = json.dumps(args["configTemplate"]) + # 未显式提供 configTemplate 时,按 datasourceId + skillId 自动生成标准模板 + elif not args.get("configTemplate"): + args["configTemplate"] = self._build_config_template( + str(args["datasourceId"]), str(args["skillId"]) + ) return await self.client.post("/datasource/skill/updateOrGet", json_data=args) @register_tool("update_skill_tool") class UpdateSkillToolTool(ToolDef): name = "update_skill_tool" - description = "修改技能下某个工具的名称/描述/SQL等(对应后端 tskilltool/updateOrGet,upsert 语义)" - # 真实工具实体字段(已真机探测确认):主键 id、展示名 uniqueName、描述 description、 - # SQL 模板 sqlTemplate、结果类型 resultType。后端不认 skillToolId/businessDescription/businessScenario。 + description = ( + "修改技能下某个工具的名称/描述/SQL等(对应后端 tskilltool/updateOrGet,upsert 语义)。" + "改展示名传 name 即可(工具会同时写 name 和 uniqueName 两个字段,与工具实体存储一致)。" + "工具名建议遵循前端约束:≤20 字、只含中英文/数字/空格、不含特殊符号。" + ) + # 工具实体同时存在 name 与 uniqueName 两个字段(真机数据里二者取值相同,见 skill.json)。 + # 前端 ChatDebugging.vue handleEditToolSubmit 改名时提交 {id, name, description}(用 name); + # 早期真机探测又得到 uniqueName。为消除歧义、与实体存储保持一致,改名时两个字段都写。 input_schema = { "type": "object", "properties": { "id": {"type": "string", "description": "技能工具 ID(get_skill_tools 返回的 id)"}, - "uniqueName": {"type": "string", "description": "工具展示名(可选)"}, + "name": {"type": "string", "description": "工具展示名(可选)。会同时写入 name 与 uniqueName"}, "description": {"type": "string", "description": "工具描述(可选)"}, "sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"}, "resultType": {"type": "string", "enum": ["single", "list"], "description": "结果类型(可选)"}, @@ -161,7 +200,6 @@ class UpdateSkillToolTool(ToolDef): # 旧参数名 -> 真实字段名(向后兼容此前错误实现的调用方) _LEGACY_MAP = { "skillToolId": "id", - "name": "uniqueName", "businessDescription": "description", } @@ -172,6 +210,177 @@ class UpdateSkillToolTool(ToolDef): args[new] = args.pop(old) else: args.pop(old, None) + # 展示名:name / uniqueName 任一传入都同步到两个字段(与工具实体存储一致, + # 兼容前端用 name、早期探测用 uniqueName 两种契约,避免改名不生效)。 + display_name = args.get("name") if args.get("name") is not None else args.get("uniqueName") + if display_name is not None: + args["name"] = display_name + args["uniqueName"] = display_name # businessScenario 后端实体无此字段,丢弃避免干扰 args.pop("businessScenario", None) return await self.client.post("/datasource/skill/tskilltool/updateOrGet", json_data=args) + + +@register_tool("add_sql_tool_to_datasource") +class AddSqlToolToDatasourceTool(ToolDef): + name = "add_sql_tool_to_datasource" + description = ( + "把一条 SQL 沉淀为数据源的可复用工具(一步到位,推荐用这个而不是手动拼 " + "update_skill_config/create_sql_tool)。\n" + "【为什么用它】技能(skill)必须挂着工具才有效,单独建技能会留下无效的空技能。本工具内部" + "1:1 复刻前端 handleAddToolSubmit 的完整链路,保证技能必有工具:\n" + " 读 skillBool → 技能不存在则 createOrGet 建技能 → getByDatasource 拿真实 skillId →" + " 按 sqlTemplate 去重 → 技能新建时写 lzwcai-mcp-sqlexecutor 配置模板 → confirmTools 建工具。\n" + "【幂等/去重】同一 datasourceId 下若已存在 sqlTemplate 相同的工具,直接返回 skipped,不重复创建。\n" + "datasourceId 必填且为真实 ID(来自 list_databases / list_tables_with_ai / get_connection_config_list)。" + ) + input_schema = { + "type": "object", + "properties": { + "datasourceId": {"type": "string", "description": "数据源/配置 ID(真实 ID,不可臆造)"}, + "name": {"type": "string", "description": "工具名称(展示名)"}, + "businessDescription": {"type": "string", "description": "工具的业务描述"}, + "sqlTemplate": {"type": "string", "description": "SQL 模板(支持 #{param} 参数占位)"}, + "sqlParams": { + "type": "string", + "description": "参数定义,最终以 JSON 字符串提交(传 dict 会自动 json.dumps)。内容形态后端不挑剔(JSON Schema 对象串或字段定义数组串均可);不传默认空 schema", + }, + "resultType": { + "type": "string", + "enum": ["single", "list"], + "default": "list", + "description": "结果类型,默认 list", + }, + "businessScenario": {"type": "string", "description": "业务场景描述(可选)"}, + "tableIds": { + "type": "array", + "items": {"type": "string"}, + "description": "关联的表 ID 数组(可选)", + }, + "skillName": {"type": "string", "description": "技能不存在时新建技能用的名称(可选,不传自动生成)"}, + "skillDescription": {"type": "string", "description": "技能不存在时新建技能用的描述(可选)"}, + }, + "required": ["datasourceId", "name", "businessDescription", "sqlTemplate"], + } + + @staticmethod + def _unwrap(resp): + """从 {code,msg,data} 信封里取 data;非信封则原样返回。""" + if isinstance(resp, dict) and "data" in resp and ("code" in resp or "msg" in resp): + return resp["data"] + return resp + + @staticmethod + def _normalize_sql(sql) -> str: + """归一化 SQL 用于去重比较:折叠空白 + strip(与前端 replace(/\\s+/g,' ').trim() 一致)。""" + if not isinstance(sql, str): + return "" + return " ".join(sql.split()).strip() + + async def _get_skill_id(self, datasource_id: str): + """getByDatasource 拿技能 id;拿不到返回 None。""" + resp = await self.client.get(f"/datasource/skill/getByDatasource/{datasource_id}") + data = self._unwrap(resp) + if isinstance(data, dict): + return data.get("id") + return None + + async def execute(self, args: dict) -> dict: + args = dict(args) + datasource_id = str(args["datasourceId"]) + sql_template = args["sqlTemplate"] + + # 1. 读数据源配置,判断 skillBool(技能是否已存在) + config_resp = await self.client.get(f"/datasource/config/{datasource_id}") + config_data = self._unwrap(config_resp) + skill_bool = config_data.get("skillBool") if isinstance(config_data, dict) else None + + created_skill = False + # 2. 技能不存在 → 先建技能(createOrGet) + if skill_bool is not True: + create_skill_body = {"datasourceId": datasource_id} + if args.get("skillName"): + create_skill_body["name"] = args["skillName"] + if args.get("skillDescription"): + create_skill_body["description"] = args["skillDescription"] + await self.client.post("/datasource/skill/createOrGet", json_data=create_skill_body) + created_skill = True + + # 3. 拿真实 skillId(注意:id 来自 getByDatasource,不是 createOrGet 的返回) + skill_id = await self._get_skill_id(datasource_id) + if not skill_id: + raise ValueError( + f"未能获取数据源 {datasource_id} 的技能 ID(getByDatasource 未返回 id)。" + "请确认 datasourceId 正确、技能创建是否成功。" + ) + skill_id = str(skill_id) + + # 4. 去重:同 sqlTemplate 的工具已存在则跳过 + tools_resp = await self.client.get(f"/datasource/skill/getBySkillId/{skill_id}") + tools_data = self._unwrap(tools_resp) + target_norm = self._normalize_sql(sql_template) + if isinstance(tools_data, list): + for tool in tools_data: + if isinstance(tool, dict) and self._normalize_sql(tool.get("sqlTemplate")) == target_norm: + return { + "skipped": True, + "reason": "已存在 sqlTemplate 相同的工具,未重复创建", + "skillId": skill_id, + "existingTool": { + "id": tool.get("id"), + "uniqueName": tool.get("uniqueName") or tool.get("name"), + }, + } + + # 5. 技能是本次新建的 → 写入 lzwcai-mcp-sqlexecutor 标准配置模板 + if created_skill: + config_template = UpdateSkillConfigTool._build_config_template(datasource_id, skill_id) + await self.client.post( + "/datasource/skill/updateOrGet", + json_data={"datasourceId": datasource_id, "configTemplate": config_template}, + ) + + # 6. confirmTools 建工具 + sql_params = args.get("sqlParams") + if isinstance(sql_params, dict): + sql_params = json.dumps(sql_params) + elif not sql_params: + sql_params = '{"type":"object","required":[],"properties":{}}' + + suggestion = { + "name": args["name"], + "businessDescription": args["businessDescription"], + "sqlTemplate": sql_template, + "sqlParams": sql_params, + "resultType": args.get("resultType", "list"), + "businessScenario": args.get("businessScenario", "数据查询场景"), + } + # tableIds:前端真机始终传 ""(空串)。None / 空列表都归一为 "",与前端一致; + # 仅当调用方显式给了非空列表时才透传该列表。 + table_ids = args.get("tableIds") + confirm_body = { + "skillId": skill_id, + "tableIds": table_ids if table_ids else "", + "suggestions": [suggestion], + } + try: + confirm_result = await self.client.post( + "/datasource/skill/confirmTools", json_data=confirm_body + ) + except Exception as e: + # 工具创建失败:若本次刚建了技能,此刻技能是「空技能」(有配置无工具)—— + # 正是要避免的无效状态。明确告知调用方:技能已建好,重跑本工具即可补上工具 + # (重跑会走 skillBool=true 分支,仅补工具,幂等安全)。 + if created_skill: + raise Exception( + f"技能已创建(skillId={skill_id})但工具创建失败:{e}。" + "当前技能为「空技能」,请用相同参数重新调用本工具补上工具" + "(重跑只会补工具、不会重复建技能)。" + ) from e + raise + return { + "success": True, + "skillId": skill_id, + "skillCreated": created_skill, + "result": confirm_result, + } diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt new file mode 100644 index 0000000..5acef76 --- /dev/null +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt @@ -0,0 +1,1139 @@ + + + + + \ No newline at end of file diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__init__.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__init__.py index 1b6c953..f6888ba 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__init__.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__init__.py @@ -1,4 +1,10 @@ -from .env_config import get_api_key, get_base_url, get_env_config +from .env_config import ( + get_api_key, + get_base_url, + get_env_config, + get_account, + get_password, +) from .logger_config import setup_system_logging, get_logger from .api_client import AgileDBAPIClient, get_default_client @@ -6,6 +12,8 @@ __all__ = [ 'get_api_key', 'get_base_url', 'get_env_config', + 'get_account', + 'get_password', 'setup_system_logging', 'get_logger', 'AgileDBAPIClient', 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 94343c0ef5e9a91945833a1706b7cd996b82a6d9..fef48faecec0fb4438814df30cb03a5c3169d8e8 100644 GIT binary patch delta 370 zcmYL^KTE?v7{>4Nr)iU(AWlVaaViq%s&o}x6c?8xaGaO(ntGRnOKfQu@gsx|ZVqmK z4JX0HIg7KW;(9!j$x--?)G$Ziw+D<`l$AKIw34eFA`BLd)GCRnGjsBpLDy) zb_{Bt<_Tdk;c5BbPDG7N1pC$yp^TUH^HVSDHU+Fj#E>ID7@e562Iz J8&7YV$PeL6Xnp_y delta 205 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 uj#$Hd?o#0BTs3*aQu7BXo61E1 delta 877 zcmZWnTS!xJ7(f4W-8s8Br`Vimr^NKoLsC{aMDSrN_wRoes~(g)zS!*& z1ybfZesh>hI@~CjY^=LzQ(2AC*lO^ooW=%Om7nDLIMoh3TVe!Yc)&uCRxOikpG6hH zCz*bg4)UrMIB|kj9LdiU=mu#+T@828ZKPWxg4xj&m3fQy&^_?rhE6vD}j-y=EFD) zIoV}m=J~sP*X7OWH`{ZcwwC97ndpiOzUuqC4K5Zk-vL9bIs(+FD}1HLG*o;m%aIt~$IKi}$P58N0Ack$2nxS|nxs zA+~g_s}>WOLf}T^1%K?c!e9}qWAl}<6_kRCL+Iw=W$XZL*kDfoKtR|0Q9Obxfbx)E zd<$9PM05=vu9b`kR`OnOiOe)ze5HPp5lUSP!kH$8HiW>fkbhX4Ff4xxDeOO;aRnK* z$Q^E|-w2=H01CEtQk7LHZ_=A;Pqxn}uX|Q2n%663(p}&f8=t2bj>k@jG#IFk?SROJ zCr1jm^In+sTS}n5zNULUzPXn`8)nzZxF&`>{xT(m|KOs-eLM zHmAX67(PH^lDk(1Xr4yz2bE_KLTgmV1|@!@Y6`p)m8ab^_0Mbb6u^c0UDFhfNa$p` ZK2L#JIk9V+yhlQOnl&+>_mFC9?Qduv_9y@V 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 19e8ed7..00fa4bb 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 @@ -3,12 +3,17 @@ 用于调用数据库管理平台的所有 API 接口 """ +import asyncio import httpx import json -import os from typing import Dict, Any, Optional -from .env_config import get_api_key, get_base_url +from .env_config import ( + get_api_key, + get_base_url, + get_account, + get_password, +) from .logger_config import get_logger logger = get_logger(__name__) @@ -16,53 +21,196 @@ logger = get_logger(__name__) # 默认超时配置(秒) DEFAULT_TIMEOUT = 30.0 +# 登录接口路径(base_url 已含 /api 前缀,此处不重复带) +LOGIN_PATH = "/login" + +# 登录类型(平台固定为 user) +LOGIN_TYPE = "user" + class AgileDBAPIClient: - """数据库管理平台 API 客户端""" - + """数据库管理平台 API 客户端 + + 认证支持两种方式(优先级从高到低): + 1. 显式 api_key / 环境变量 API_KEY —— 直接作为 Bearer token 使用; + 2. 账号密码(环境变量 AGILE_DB_ACCOUNT / AGILE_DB_PASSWORD)—— 懒登录, + 首次请求时自动调用 /login 换取 token 并缓存;token 失效(401)时 + 自动重新登录并重试一次。 + """ + def __init__( - self, + self, base_url: Optional[str] = None, api_key: Optional[str] = None, + account: Optional[str] = None, + password: Optional[str] = None, default_timeout: float = DEFAULT_TIMEOUT, ): """ 初始化 API 客户端 - + Args: base_url: API 基础 URL(默认从环境变量 backendBaseUrl 读取) - api_key: API 密钥(默认从环境变量 API_KEY 读取) + api_key: API 密钥(默认从环境变量 API_KEY 读取,可为空) + account: 登录账号(默认从环境变量 AGILE_DB_ACCOUNT 读取) + password: 登录密码(默认从环境变量 AGILE_DB_PASSWORD 读取) default_timeout: 请求超时时间(秒),默认 30 秒 """ - if base_url is None: - base_url = get_base_url() - - if api_key is None: - api_key = get_api_key() - - self.base_url = base_url.rstrip('/') - self.api_key = api_key + self.base_url = (base_url if base_url is not None else get_base_url()).rstrip('/') + # 显式配置的 api_key 直接作为 token 使用(去掉可能存在的 Bearer 前缀,统一在 _get_headers 拼) + explicit_key = api_key if api_key is not None else get_api_key() + self._token: Optional[str] = self._strip_bearer(explicit_key) or None + + self.account = account if account is not None else get_account() + self.password = password if password is not None else get_password() + self.default_timeout = default_timeout self._client: Optional[httpx.AsyncClient] = None - - logger.info(f"[客户端初始化] base_url={self.base_url}") - + # 串行化登录,避免并发请求同时触发多次登录 + self._login_lock = asyncio.Lock() + + logger.info( + f"[客户端初始化] base_url={self.base_url}, " + f"认证方式={'api_key' if self._token else ('account:' + self.account if self.account else '未配置')}" + ) + + @staticmethod + def _strip_bearer(value: Optional[str]) -> str: + """去掉 token 字符串可能携带的 'Bearer ' 前缀""" + if not value: + return "" + value = value.strip() + return value[7:].strip() if value.lower().startswith("bearer ") else value + @property def client(self) -> httpx.AsyncClient: """懒加载 HTTP 客户端""" if self._client is None: self._client = httpx.AsyncClient(timeout=self.default_timeout) return self._client - + + async def _ensure_token(self) -> str: + """确保已有可用 token,没有则登录获取""" + if self._token: + return self._token + return await self._login() + + async def _login(self) -> str: + """换取 token 并缓存(并发安全,用于首次登录)""" + async with self._login_lock: + # 双重检查:可能在等锁期间已有其它协程完成登录 + if self._token: + return self._token + return await self._do_login() + + async def _relogin(self, stale_token: Optional[str]) -> str: + """登录态失效后重新登录(compare-and-swap,并发安全)。 + + 仅当当前 token 仍是那次失败请求所用的旧 token 时才真正重登; + 若在等锁期间已有其它协程刷新过 token,则直接复用新 token, + 避免把别人刚拿到的新 token 抹掉又触发一次多余的重登。 + """ + async with self._login_lock: + if self._token != stale_token: + # 别的协程已经刷新过 token,直接用新的 + return self._token or "" + self._token = None + return await self._do_login() + + async def _do_login(self) -> str: + """实际执行 /login 的逻辑(调用方需自行持有 _login_lock)。""" + if not self.account or not self.password: + raise Exception( + "未配置认证信息:请设置环境变量 API_KEY," + "或同时设置 AGILE_DB_ACCOUNT 和 AGILE_DB_PASSWORD" + ) + + url = self._build_url(LOGIN_PATH) + payload = { + "username": self.account, + "password": self.password, + "loginType": LOGIN_TYPE, + } + logger.info(f"[登录] POST {url}, username={self.account}, loginType={LOGIN_TYPE}") + try: + response = await self.client.post( + url, + headers={"Content-Type": "application/json"}, + json=payload, + ) + except httpx.TimeoutException: + raise Exception(f"登录请求超时: {url}") + except httpx.RequestError as e: + raise Exception(f"登录请求异常: {url}, 错误: {str(e)}") + + is_json, body = self._try_parse_json(response) + data = self._handle_response(response, url, is_json, body) + # 平台登录响应:token 在顶层 token 字段 + token = data.get("token") if isinstance(data, dict) else None + if not token: + raise Exception(f"登录成功但未返回 token,响应: {json.dumps(data, ensure_ascii=False)[:300]}") + + self._token = self._strip_bearer(token) + logger.info("[登录] 成功获取 token") + return self._token + + @staticmethod + def _try_parse_json(response: httpx.Response): + """尝试把响应体解析为 JSON,只解析一次供后续复用。 + + Returns: + (is_json, data):是 JSON 则 (True, 解析结果); + 二进制/非 JSON 响应(如 Excel 下载)则 (False, None)。 + """ + try: + return True, response.json() + except (json.JSONDecodeError, UnicodeDecodeError): + return False, None + + @staticmethod + def _is_unauthorized(response: httpx.Response, is_json: bool, data: Any) -> bool: + """判断响应是否为登录态失效(基于已解析好的 body,不重复 parse)。 + + 平台有两种表达 401 的方式,都需识别: + 1. HTTP 状态码 401; + 2. HTTP 200 但 body 信封里 code=401(如 {"code":401,"msg":"登录过期,请重新登录"})。 + """ + if response.status_code == 401: + return True + return is_json and isinstance(data, dict) and data.get("code") == 401 + + @staticmethod + def _rewind_files(files: Optional[Dict[str, Any]]) -> None: + """把上传用的文件流游标重置到开头。 + + 401 重试会复用同一个 files 二次发送,而文件流在第一次发送后游标已到末尾, + 不 rewind 会导致重试上传空内容。支持两种形态: + - 直接的文件对象; + - (filename, fileobj, content_type) 元组(httpx multipart 常用写法)。 + """ + if not files: + return + for value in files.values(): + fileobj = value + if isinstance(value, (tuple, list)) and len(value) >= 2: + fileobj = value[1] + seek = getattr(fileobj, "seek", None) + if callable(seek): + try: + seek(0) + except (OSError, ValueError): + # 不可重置的流(如已关闭/不支持 seek)静默跳过,交由上传结果反映 + pass + def _get_headers(self, extra_headers: Optional[Dict[str, str]] = None) -> Dict[str, str]: """获取请求头""" - headers = { - 'Authorization': self.api_key if self.api_key.startswith('Bearer ') else f'Bearer {self.api_key}', - } + headers = {} + if self._token: + headers['Authorization'] = f'Bearer {self._token}' if extra_headers: headers.update(extra_headers) return headers - + def _build_url(self, path: str) -> str: """构建完整 URL @@ -72,20 +220,32 @@ class AgileDBAPIClient: if path.startswith('http://') or path.startswith('https://'): return path return f"{self.base_url}{path}" - - def _handle_response(self, response: httpx.Response, url: str) -> Dict[str, Any]: - """统一处理 API 响应""" + + def _handle_response( + self, + response: httpx.Response, + url: str, + is_json: Optional[bool] = None, + data: Any = None, + ) -> Dict[str, Any]: + """统一处理 API 响应 + + is_json / data 为调用方已解析好的 body(避免对大响应重复 parse); + 未传入时此处自行解析一次。 + """ logger.info(f"[API响应] HTTP {response.status_code}") if response.status_code == 204: return {"success": True, "data": None} - # 先尝试解析 body,再判断状态码。 + # 复用调用方解析结果;未提供则在此解析一次 + if is_json is None: + is_json, data = self._try_parse_json(response) + + # 先看 body 再判断状态码。 # 平台会用非 2xx 状态码 + body 里的 {code, msg} 返回业务错误, - # 若先 raise_for_status() 会在解析 body 前抛异常,导致真正的 msg 全部丢失。 - try: - data = response.json() - except (json.JSONDecodeError, UnicodeDecodeError): + # 若先 raise_for_status() 会在拿到 body 前抛异常,导致真正的 msg 全部丢失。 + if not is_json: # 非 JSON 响应(如 Excel/文件下载,二进制内容 .json() 会抛 UnicodeDecodeError) response.raise_for_status() return {"success": True, "data": response.content, "raw": True} @@ -108,14 +268,53 @@ class AgileDBAPIClient: raise Exception(f"HTTP {response.status_code}: {detail}") return data - - async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """发送 GET 请求""" + + async def _request( + self, + method: str, + path: str, + *, + params: Optional[Dict[str, Any]] = None, + json_data: Optional[Dict[str, Any]] = None, + files: Optional[Dict[str, Any]] = None, + extra_headers: Optional[Dict[str, str]] = None, + ) -> Dict[str, Any]: + """统一请求入口:自动注入认证、登录态失效(401)时重登重试一次""" url = self._build_url(path) + # 账号密码模式下首次请求前先确保有 token;纯 api_key 模式 _ensure_token 直接返回 + await self._ensure_token() + + async def _send() -> httpx.Response: + headers = self._get_headers(extra_headers) + return await self.client.request( + method, url, headers=headers, params=params, json=json_data, files=files + ) + try: - logger.info(f"[API请求] GET {url}") - response = await self.client.get(url, headers=self._get_headers(), params=params) - return self._handle_response(response, url) + logger.info(f"[API请求] {method} {url}") + # 记下本次请求所用 token,供 401 时做 compare-and-swap 重登 + token_used = self._token + response = await _send() + is_json, body = self._try_parse_json(response) + + # token 失效:仅在账号密码模式下尝试重新登录并重试一次。 + # 平台可能用 HTTP 401,也可能用 HTTP 200 + body code=401 表达登录过期, + # 两者都要识别(见 _is_unauthorized)。 + if self._is_unauthorized(response, is_json, body) and self.account and self.password: + logger.warning("[认证] 收到 401(登录过期),尝试重新登录后重试一次") + # CAS 重登:仅当 token 仍是本次用的旧值才真正重登,否则复用别人刚拿到的新 token + await self._relogin(token_used) + # 重试前把上传文件流游标重置到开头,避免二次发送空内容 + self._rewind_files(files) + response = await _send() + is_json, body = self._try_parse_json(response) + + # 重登后仍判定为登录态失效:账号密码本身失效/被禁,给明确报错 + if self._is_unauthorized(response, is_json, body): + logger.error("[认证] 重新登录后仍返回 401") + raise Exception("重新登录后仍未通过认证,请检查账号密码是否正确或账号是否被禁用") + + return self._handle_response(response, url, is_json, body) except httpx.TimeoutException: raise Exception(f"API 请求超时: {url}") except httpx.HTTPStatusError as e: @@ -123,82 +322,48 @@ class AgileDBAPIClient: except httpx.RequestError as e: raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") + async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: + """发送 GET 请求""" + return await self._request("GET", path, params=params) + async def post(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """发送 POST 请求""" - url = self._build_url(path) - try: - logger.info(f"[API请求] POST {url}") - headers = self._get_headers({'Content-Type': 'application/json'}) - response = await self.client.post(url, headers=headers, json=json_data, params=params) - return self._handle_response(response, url) - except httpx.TimeoutException: - raise Exception(f"API 请求超时: {url}") - except httpx.HTTPStatusError as e: - raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}") - except httpx.RequestError as e: - raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") + return await self._request( + "POST", path, params=params, json_data=json_data, + extra_headers={'Content-Type': 'application/json'}, + ) async def put(self, path: str, json_data: Optional[Dict[str, Any]] = None, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: """发送 PUT 请求""" - url = self._build_url(path) - try: - logger.info(f"[API请求] PUT {url}") - headers = self._get_headers({'Content-Type': 'application/json'}) - response = await self.client.put(url, headers=headers, json=json_data, params=params) - return self._handle_response(response, url) - except httpx.TimeoutException: - raise Exception(f"API 请求超时: {url}") - except httpx.HTTPStatusError as e: - raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}") - except httpx.RequestError as e: - raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") + return await self._request( + "PUT", path, params=params, json_data=json_data, + extra_headers={'Content-Type': 'application/json'}, + ) async def delete(self, path: str, params: Optional[Dict[str, Any]] = None, json_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """发送 DELETE 请求""" - url = self._build_url(path) - try: - logger.info(f"[API请求] DELETE {url}") - headers = self._get_headers() - # 注意: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.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}") - except httpx.HTTPStatusError as e: - raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}") - except httpx.RequestError as e: - raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") + """发送 DELETE 请求 + + 平台部分 DELETE 接口需带 body,统一走通用 request() 处理。 + """ + extra = {'Content-Type': 'application/json'} if json_data is not None else None + return await self._request("DELETE", path, params=params, json_data=json_data, extra_headers=extra) async def upload(self, path: str, files: Dict[str, Any], params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]: - """发送文件上传请求(multipart/form-data)""" - url = self._build_url(path) - try: - logger.info(f"[API请求] UPLOAD {url}") - # 文件上传不需要 Content-Type,httpx 会自动设置 multipart/form-data - headers = self._get_headers() - response = await self.client.post(url, headers=headers, files=files, params=params) - return self._handle_response(response, url) - except httpx.TimeoutException: - raise Exception(f"API 请求超时: {url}") - except httpx.HTTPStatusError as e: - raise Exception(f"API 请求失败 (HTTP {e.response.status_code}): {url}") - except httpx.RequestError as e: - raise Exception(f"API 请求异常: {url}, 错误: {str(e)}") + """发送文件上传请求(multipart/form-data) + + 不显式设置 Content-Type,httpx 会根据 files 自动生成 multipart 边界。 + """ + return await self._request("POST", path, params=params, files=files) async def close(self): """关闭 HTTP 客户端""" if self._client is not None: await self._client.aclose() self._client = None - + async def __aenter__(self): return self - + async def __aexit__(self, exc_type, exc_val, exc_tb): await self.close() return False 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 87364db..a657a6d 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 @@ -6,30 +6,37 @@ from typing import Optional def get_api_key(default: Optional[str] = None) -> str: """ - 获取 API 密钥 - + 获取 API 密钥(可选) + + 优先级:显式配置了 API_KEY 时直接使用;未配置则返回空串, + 由客户端回落到账号密码登录流程换取 token。 + Args: default: 默认值(可选) - + Returns: - str: API 密钥 - - Raises: - ValueError: 当 API_KEY 未设置且无默认值时 + str: API 密钥,未配置时为空串 """ - value = os.environ.get("API_KEY", default or "") - if not value: - raise ValueError("环境变量 API_KEY 未设置") - return value + return os.environ.get("API_KEY", default or "") + + +def get_account(default: Optional[str] = None) -> str: + """获取登录账号(环境变量 AGILE_DB_ACCOUNT)""" + return os.environ.get("AGILE_DB_ACCOUNT", default or "") + + +def get_password(default: Optional[str] = None) -> str: + """获取登录密码(环境变量 AGILE_DB_PASSWORD)""" + return os.environ.get("AGILE_DB_PASSWORD", default or "") def get_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str: """ 获取后端服务地址 - + Args: default: 默认值(默认 http://lzwcai-demp-corp-manager:8086) - + Returns: str: 后端 API 基础 URL """ @@ -39,12 +46,13 @@ def get_base_url(default: str = "http://lzwcai-demp-corp-manager:8086") -> str: def get_env_config() -> dict: """ 获取所有环境配置 - + Returns: dict: 包含所有配置的字典 """ return { "api_key": os.environ.get("API_KEY", ""), + "account": os.environ.get("AGILE_DB_ACCOUNT", ""), "base_url": get_base_url(), } @@ -52,7 +60,7 @@ def get_env_config() -> dict: def set_env_variable(key: str, value: str) -> None: """ 设置环境变量(仅在当前进程中有效) - + Args: key: 环境变量名 value: 环境变量值 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 70deb5e..d3d9de6 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-MCP工具设计方案.md +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库管理平台-MCP工具设计方案.md @@ -385,15 +385,9 @@ --- -#### 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` | - -**返回**:撤销结果 +#### 23.5 ~~`revoke_api_key_permissions`~~(已废弃,不提供) +- **结论**:权限为「仅追加」模型。真机验证后端 permission 删除接口返回「不支持当前的调用方式」,无法撤销单条已授予权限,故不实现该工具。 +- **替代方案**:要缩小某密钥的权限范围,只能「删密钥(`delete_api_key`)→ 重建(`create_api_key`)→ 重新授权(`grant_api_key_permissions`)」。 --- @@ -423,17 +417,9 @@ --- -#### 26. `create_skill` -- **用途**:为数据源创建技能 -- **对应 API**:`postSkillCreateOrGet(data)` ✅ 已实现 — `POST /api/datasource/skill/createOrGet` - -| 参数 | 类型 | 必填 | 说明 | -|------|------|------|------| -| datasourceId | string | 是 | 数据源 ID | -| name | string | 否 | 技能名称(不传则自动生成) | -| description | string | 否 | 技能描述 | - -**返回**:技能 ID +#### 26. ~~`create_skill`~~(已移除,不再单独暴露) +- **结论**:技能(skill)必须挂着工具才有效,平时不会单独创建技能;单独建技能会留下无效的空技能。前端也没有「只建技能」入口。 +- **替代方案**:把 SQL 沉淀为工具统一用 `add_sql_tool_to_datasource`(见 §29.5),它内部按需调 `skill/createOrGet` 建技能、写配置模板、再 `confirmTools` 建工具,一步到位且保证技能必有工具。底层 `skill/createOrGet` 端点仍被该编排工具内部使用,只是不再作为独立 MCP 工具暴露。 --- @@ -507,9 +493,15 @@ | 参数 | 类型 | 必填 | 说明 | |------|------|------|------| | connectionId | int | 是 | 数据源 ID | +| databaseName | string | 是 | 落库目标库名 | | target | string | 否 | prod/test | | data | object | 是 | 导入数据(含 tableStructure + allData) | +**data.tableStructure.columns 的关键约束**: +- `columns` 定义了「这批数据要写入哪些字段」,**导入时这些列必须对应目标表中真实存在的字段**——列名(`columnName`)要与目标表的实际字段名一致,否则后端按列名拼 INSERT 时会报「查询字段不存在 / 字段名称不正确」。 +- `allData` 的**首行是列名表头**(= `columns[].columnName`),数据从第 2 行起;每行按 `columns` 顺序给出**全部列**的值(全列宽,不裁剪)。表头列名同样必须是目标表存在的字段。 +- **导入到已有表时**:前端会用目标表的真实列(`get_table_detail` 返回的 columns)覆盖 AI 识别的列。MCP 调用方应等价处理——**先 `get_table_detail` 拿到目标表真实字段,再让 data 里的 columns / 表头 / 数据与之对齐**,不要直接用 Excel 识别出的、可能与表字段不符的列。 + **返回**:导入结果 --- diff --git a/lzwcai_mcp_agile_db/main.py b/lzwcai_mcp_agile_db/main.py index d86d2da..b4fb4ef 100644 --- a/lzwcai_mcp_agile_db/main.py +++ b/lzwcai_mcp_agile_db/main.py @@ -7,6 +7,8 @@ from lzwcai_mcp_agile_db.server import main import os if __name__ == "__main__": - os.environ["API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6ImNlMDAwYjA4LWU0YTYtNGM2MS1hNzJiLWI3NTlmNmY1N2Q4NCJ9.jiNmGQZfL4-nSIFrLuaCt7mT5zj0FOojAVkLeHwPOroI5jBxodrCe1PSwGO1OHq5Ztb0tLEVZw2FFVj0OlTceQ" - os.environ["backendBaseUrl"] = "http://192.168.2.236:8088" + # 账号密码方式:客户端会在首次请求时自动调用 /login 换取 token + os.environ["AGILE_DB_ACCOUNT"] = "yy8z9" + os.environ["AGILE_DB_PASSWORD"] = "lzwc@2025." + os.environ["backendBaseUrl"] = "http://192.168.2.236:8082/api" main() diff --git a/lzwcai_mcp_agile_db/pyproject.toml b/lzwcai_mcp_agile_db/pyproject.toml index 8509d38..3d48ea4 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.7" +version = "0.1.17" description = "MCP server for database management platform with 33 tools for datasource, table, data, API key, and skill management" readme = "README.md" requires-python = ">=3.10" diff --git a/lzwcai_mcp_agile_db_third/pyproject.toml b/lzwcai_mcp_agile_db_third/pyproject.toml index 4c9dc91..dc63aa0 100644 --- a/lzwcai_mcp_agile_db_third/pyproject.toml +++ b/lzwcai_mcp_agile_db_third/pyproject.toml @@ -4,10 +4,10 @@ build-backend = "hatchling.build" [project] name = "lzwcai-mcp-agile-db-third" -version = "0.1.5" +version = "0.1.6" description = "MCP server for Agile DB third-party datasource APIs" readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.10" license = {text = "MIT"} authors = [ {name = "lzwcai", email = "your-email@example.com"}, diff --git a/lzwcai_mcpskills_generate_reports/README.md b/lzwcai_mcpskills_generate_reports/README.md index 493f2ef..cf95d8c 100644 --- a/lzwcai_mcpskills_generate_reports/README.md +++ b/lzwcai_mcpskills_generate_reports/README.md @@ -13,26 +13,55 @@ pip install -e . ## Python API -### 渲染文档 +### 核心入口 `generate` / `scan_template` ```python -from lzwcai_mcpskills_generate_reports import generate +from lzwcai_mcpskills_generate_reports import generate, scan_template +# 扫描模板需要哪些占位符 / for / if 块 +result = scan_template("./模板.docx") # 本地路径或 http/https URL + +# 渲染 out_path = generate( data="data.json", # dict 或 JSON 文件路径 - template="./模板.docx", # 用户自己的 docx 模板路径 + template="./模板.docx", # 本地路径或 http/https URL(自动下载) out_path="_out/报价方案.docx", style_ref="./用户样式.docx", # 可选:把用户模板的 theme/字体套过来 ) ``` -### 扫描模板占位符 +### 便捷封装 `main` 模块 + +`main` 模块在核心入口之上做了两点增强,适合程序化调用: + +1. `data` 除 dict / 本地 JSON 路径外,还支持 **JSON 文件 URL**(自动下载、用完即删)。 +2. `out` **可省略**;省略时落到当前目录 `_out/`,文件名按 `模板名_时间戳.docx` 自动生成。 ```python -from lzwcai_mcpskills_generate_reports import scan_template +from lzwcai_mcpskills_generate_reports.main import generate_report, scan_report -result = scan_template("./模板.docx") -print(result) +# data 传 dict,out 省略 -> 默认 _out/ 下自动命名 +generate_report(template="./模板.docx", data={...}) + +# data 传本地 JSON 路径 +generate_report(template="./模板.docx", data="data.json", out="_out/a.docx") + +# template / data 都传 URL +generate_report( + template="https://host/模板.docx", + data="https://host/data.json", +) + +# 扫描占位符,支持本地路径或 URL +scan_report(template="https://host/模板.docx") +``` + +`generate_report` 返回 `{"output": 输出文件绝对路径}`。 + +### 扫描结果结构 + +```python +# scan_template / scan_report 返回: # { # "placeholders": ["project_title", "contact_person", "equipments", ...], # "blocks": [ @@ -43,30 +72,16 @@ print(result) # } ``` -## 命令行 - -```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 工具: +本包同时提供 MCP Server(stdio 模式),把渲染引擎暴露成 2 个 MCP 工具: -| 工具 | 说明 | 必填参数 | -|------|------|----------| -| `generate_report` | 模板 + 数据 → 渲染输出 docx,返回输出文件绝对路径 | `template`, `data`, `out`(可选 `style_ref`) | -| `scan_template` | 扫描模板占位符与 for/if 块结构 | `template` | -| `validate_report_data` | 校验数据契约(不渲染) | `data` | +| 工具 | 说明 | 参数 | +|------|------|------| +| `generate_report` | 模板 + 数据 → 渲染输出 docx,返回输出文件绝对路径 | 必填 `template`、`data`;可选 `out`(省略落到 `_out/` 自动命名)、`style_ref` | +| `scan_template` | 扫描模板占位符与 for/if 块结构 | 必填 `template` | -其中 `data` 既可以传 JSON 对象,也可以传指向 JSON 文件的路径字符串。 +其中 `data` 既可以传 JSON 对象,也可以传指向 JSON 文件的路径或 URL 字符串;`template` 支持本地路径或 http/https URL。数据契约校验由 `generate_report` 内部自动完成(不合法会报错)。 ### 启动 @@ -74,8 +89,8 @@ generate-report generate --template ./模板.docx --data data.json --style-ref . # 安装后用 console script 启动 lzwcai-mcpskills-generate-reports -# 或直接运行入口模块 -python main.py +# 或以模块方式运行 +python -m lzwcai_mcpskills_generate_reports.server ``` ### MCP 客户端配置示例 @@ -92,6 +107,13 @@ python main.py > stdio 模式下 stdout 被 MCP 协议占用,所有日志写入 `logs/` 目录与 stderr。 +## 环境变量 + +| 变量 | 默认 | 说明 | +|------|------|------| +| `LOG_LEVEL` | `INFO` | 日志级别(DEBUG/INFO/WARNING/ERROR/CRITICAL)。 | +| `LZWCAI_INSECURE_SSL` | 关闭 | 设为 `1`/`true`/`yes` 时,下载模板/数据/图片**关闭 SSL 证书校验**。仅用于内网自签名证书等可信场景,生产慎用。 | + ## 数据契约(QuoteData) ```json @@ -117,7 +139,11 @@ python main.py } ``` -图片字段约定:路径/URL → 真实图;`""` → 默认占位图;`None` → 不显示。 +- 必填顶层字段:`project_title`、`contact_person`、`contact_phone`、`requirements`。 +- `requirements` 传列表会自动拼成多行字符串。 +- `equipments[].index` 省略时自动从"四"起按中文数字补全(前三章固定为公司简介 / 客户要求 / 布局图)。 +- `params[].v` 允许为 `0`、空串等合法假值;仅当键缺失或值为 `null` 才算缺失。 +- 图片字段约定:路径/URL → 真实图;`""` → 默认占位图;`None` → 不显示。 ## 目录结构 @@ -125,20 +151,23 @@ python main.py lzwcai_mcpskills_generate_reports/ ├── pyproject.toml ├── README.md -├── templates/ # 用户模板(示例,不在包内) +├── main.py # 使用本包的示例脚本(仓库根,非包内) +├── templates/ # 用户模板(示例,不在包内) │ └── standard/ │ ├── template.docx │ └── meta.json -├── samples/ # 示例数据(不在包内) +├── 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 # 模板占位符扫描 +└── lzwcai_mcpskills_generate_reports/ # Python 包 + ├── __init__.py # 公共 API 入口 + ├── main.py # 程序化便捷封装(URL data / out 可省略) + ├── server.py # MCP Server (stdio) + ├── pipeline.py # 总入口 + ├── schema.py # 数据契约 + 校验器 + ├── render_quote.py # 渲染引擎 + ├── style_transfer.py # 样式迁移 + ├── template_scanner.py # 模板占位符扫描 + └── utils/ # 下载 / 日志等工具 ``` ## 模板约定 @@ -156,4 +185,4 @@ lzwcai_mcpskills_generate_reports/ } ``` -- BUSINESS 逻辑(内置模板维护、模板市场等)交给包外的上层系统处理。 +- BUSINESS 逻辑(内置模板维护、模板市场等)交给包外的上层系统处理。 diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__init__.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__init__.py index 84f9022..f5fee7c 100644 --- a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__init__.py +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__init__.py @@ -5,9 +5,10 @@ lzwcai-mcpskills-generate-reports 纯渲染引擎,不内置模板。 对外暴露的公共 API: - generate: 数据 + 模板路径 -> docx(核心入口) - - scan_template: 模板路径 -> 占位符 JSON + - scan_template: 返回 QuoteData 数据契约结构(该传哪些字段、嵌套结构) - validate: 校验数据契约 - normalize: 归一化数据 + - describe: 返回数据契约结构(scan_template 的底层实现) - transplant_style: 将用户模板样式迁移到结果文档 """ @@ -15,15 +16,20 @@ __version__ = "0.1.0" from .pipeline import generate from .template_scanner import scan_template -from .schema import validate, normalize, DEFAULTS +from .schema import validate, normalize, describe, DEFAULTS from .style_transfer import transplant_style +from .utils.fetch import is_url, download_to_temp, local_file __all__ = [ "generate", "scan_template", "validate", "normalize", + "describe", "transplant_style", + "is_url", + "download_to_temp", + "local_file", "DEFAULTS", "__version__", ] 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 index 0fee5cb57e8a5e1b8ad3aecbccce5957ff8ba133..32c6ff90f7c4aa64fbc7b774880e8b8afbbf8046 100644 GIT binary patch delta 2123 zcmahKZEO=|_#0(aWKsyU&3w)wd`)J^V%-&T{FPW zF1UmZA)7hEh6Y)piOkImgCWZT216ny{wZk{EhYYNV;FzH*bK-o-}kys#21p#!CcFFn2@VeuVN_zV0f9aUYd724#N~NoiJ!GzTPqOHdM(i7npA@rmi-iMs=rr%nw`UOhE^`oi?b-B>a? zeqwU`!@FmW-@kHla^j1r;}iMbhDccaz3W)RFhFJ!1iwJts_(5b_`{J%JF;Tlj$he= z(LM0E3p_9av+aZWzhym^95XWT(x>n<%87_Un)OSOfZ!JbN&t~fyDr_PKK!b`*bd^f9QA%h#Zd?ED#MTlS0qvx}_okZ5Y3(d5nr=`_R)$f@M#T(uPv zlSYG}A2p1SBESHFvOG%x5QDju>p`b6rbFhZbh?}_Y*I>cD{@7N?P;Y{onag$)r)Mi zT;0hoeLd$)wknl5XM!;XKLO{coNm!O@(@??oO!2FA(_%3W?Yam%Blq^h*2?Pj2Wdz zNTp$b0H1Q3Lh4F`*up90Mb9z6MBQ%UEOag$Igk1$>uIc>SCf!*j2at_^^N_})r%KDZKodOHo;fa zb6g2Sd}2$l`Yq>C|Ke7vzi@?Wk)$8+XG5#nh6Eo6>*Qt$VAM@SV5#gv;9u`N9$aKhYKcY%==8J zME%aPBA(WZ2+3(W5>@=z10p4uX0zjt_(mjqGI5KiLl&|#f;l9HMMQ?eMyy;-tYWyk zyTF_aa0UHB((LJaZPJ|I-}<>OWiHEDoe4LeYVs%EX-zf-Z#AKtO(?;$a>At~tled&*QaRP<}bnv?vXDP>vl&E8A<5|-L)?u-pPOWBr>Qe*TmJ!T!YUhGV* z+?KJICMq_Mw~q6Pvh7Lxjs&+O<8q&;&(h~@XKfcON!NxR%bXG9RStC~ZR>mV8OGY9 z^zR?+ytwWsre?+nxcrP`*`P1wD2tEo8rwI#Z|uPEf${oObzP>QGEr6cgX!9PiIuyP z1$z>Cdoo<%;Qpbb7i&^n?IRDcZhoW#Z1J2IaMr$!y&LW^K-8JF2lM-2wIo1}`$zv|_qt%QkJx zw@{w+Ox=CN&oC!s;vzZ{iV--kODIV)sWNr4e8+ndzne%02`i^(awR{H+l58H|lfJ@V3o=aM59Q_lIEh78Qaj%3 zwUowB*!`0g)u&w}vzY*X1HNUpVSE22^xs^fo0Q12oZo|IQVVh}3 zA@C_-kp*05mM=tqcsB|HCg1|`wz$o;V-N%^I=%-Zl+i9SK$n*acsQ|v|4ht@Yk$@5{IgQU^XhSU41cc<;9t}}tS0;LQ&q=$ z^3YAaE0Q9jL?9{v)I%XhVIPHEEw;s%A+e@tU8-EO{40p3C+N+a%Ey+w#9SD5RLandHhE*{~j8kKIb4*X^Q0~-y~lk%0)#YKvW%BTiViP zXVF1a%VQ)rLJy7Ob17~1!cOs%@YBy4MeFxs>0z-HUbq_O+=l9flGlnQYK3Z{Hd&pl zkM6_T-QyCpp^642C1`KREl~L^w3v$jpTC znU<8R=Qby{uIxyKr>}-MRXtTd)`$&-+|Ua$KG5NLJ<$|R(=N}MW}9#Nx|>2}4(BsR zFSUOVRbq7K9|0Pte)Q6%cfGQ`LE 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 index 6c9bfae306e6edab1d7cb8911e423a8a15507788..f0584459f4ee6bbcfe61b40fb0cfbc0e077fffe7 100644 GIT binary patch delta 1287 zcmY*YOKclO7@pbnKD_oie$=%$*`!XBHf)?G4Q)h1c{Fu+2%(^g1FZpVjd$wU?Al>= ztfWv0Ba0nxW1VL~@RLKST0>`R|OT`IJs~+G0B7#GyIB@8JnYG=jbNKfEzyF{A zIs5D5A1vv&bX^1NJo)8fL)g|0_{;Xx@d??1j^HX~B?^$(NOFgVPb0H^^++Y|>1EwZ zloJrJ3$i=pCCf=BD~{@>ymUFuGb=p-Ed8Z7572@?QmD0Wep z1XzUNJos>S18j=t!3MO3zPbYBu~!RZ+RfKTA3 zAPNX~49{-=+ku2xd#4ehx!lwtUa3%rd*W{2(h(l=c_lZM61Jcd*^_p~{>>OY9vKlu z5~tU5CjB##eHt_}gSZdAj=bBiz)=iF-`7;GZ9-q*^XNoKmd;3KVJoty4P0Jh#dM^O zO!PsxMxMTt&(Oc5MC<`K9CB~xqO>4O^h*8!U6Rw|V}mkI>*&$V!&fQ$4C&B&c(+6bq81}x>jnl9zaMDU)rcUWX%#W z_r?kbm4PJ%B3XCw2_^?>=wo{xo~HBh{8*8bX*+y2^`={IBD?NYY6zcVhEuc_KM5DQ zJMr(uvr$H1{vP%@Tozt^9rMJNig{sy=%OYra7n>v-K|tnAl4AUvz&^!tkXckm1YfD z8a~Q6Y|yWhFTf)GCuvA8GNRNSN__)aw(ru0aAAot`;RhM?PZbiGE)MnPLStg?jJ_R zXSl>`wGw!d%RF`v@erx>hPlegaNb(Kb#7Xs^VuwXmOhuAWF@#+Q#!}|THTMbg^1i+ z5$zh!!*leWapX`KQ*E`0y32_xZai+&`EP89M`b zxBK4kRk)B36x()MRoliDZsJ+lBe>*alS_#3&wQj5 z#M-3p`m+}isjh`DwYPUZjNX8FOJUmU055PkT(>4EY@a^}1^UwdB z_a}at3_J(~ynuf94o1r7_W}v{-QH6p9fA&Z&JYVy?uLh9mA&4eD?QD)gxN*)0Of zA{q6!cR`KEZF@+RYPOwN^Imt#LOZ%|tqq`H7{A+obwFxbb&cs`D*=aJy ziFT}cpB9>7O^|PepzlBJR73kT?hzq)&s`oMNAc7k1Uup-&xQjpYZV)9+D=egLE5Ua zvS!&8)kKbGcJAUU%EIEpoa1{7;nLEUVy@|k{4U;sX>wc2jNhD_EN-o2)l#vF*UCt< zi^lebrk1kS_SSNXn_4w%AxtA=tE#rDE~6s3EA_$_K9rt6+e6E@{-*HPHSmZFdOD9I zu^%HtN0Fh2k=*X=PraviXTSG%A9weDW`1n$&fZ!iIrniqAI`_XVa$`C;19dQ`7!=* zOvq1=jHf)BqQP*Qf+M2Ml4V<%jZEh02(X58>~w^J#TK2pn!&^5o+r^UPOUB`TNG$5 ze|keg=c7~gUp-lPZj@>ZGIWHEl5VfaJZ9clV&JAA3l48r4SbGr$KCKzOcUqGqOY@i zn28a^R9BHwsu-mT{h4$W_&hzBBJcXf;6(i!-`D)hZB)SQk)ANv=3adbv&6C+vmrUW zfhxG4F$trpp=!wCmywOnGRbAoP8|WOm1QJ*@gUW3njD5Mz+n<NHhO*xiL@77@=;!x2h0GA+#f8FEp#OY;2?P}k%pJa>xq=uNRlJh z67dk59bRd{jr!+_ci?1)BPoi$rYQ^d-O(j5pTio}$lw-9XRcx5*#LJ`zq1Nsq-I7<`;3 XFQlFc-$~6R!TqE(Gr-^PhZO$-nq)|$ 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 index d257a11e25325baa278697a882ce43a6c62e1a7f..f8bca445cbda95d1f1eebbf8077532ddea511a16 100644 GIT binary patch delta 3034 zcmbVOeNYqW8Q(XbCVZU`Fv!=UKnyDAc{o-*dtUWA>T%ww=b@e#Lw2>%kZ_x<70NV# zaDWX07Z65*C4L}&1uR+va^mzarrvbOOxwxiFQ#U9!(YN(|LR|T-d&Qz&U8A{o!KYP zKF{<1p3nDv6WsjG2dS@8QnUm-fB)x>^Q!edsd}`d$FNOHXN=S*>G40yShO=NKhF=%D5RKC|!f* zB651uRQeQMT-<~_cJBv@yaH)K=E?=0HU(gN{-_`t2=AgY6J>e7QT+m~@%~wL4(Yw` zst@Ps8J3V+3D#;u*9^%eik{BOF0KddIQcPthXSw)N6CvK(u zMS+sMOnOz89vmBG5n0)m2)g8y!52v~rQZ4UO{m(Nl3~cl-Hj-%c8b2VfV<0NN`e|F z0h8=Kl2MtIg~e+y$?@LL$Wd*;Y_9iJM#)YLAJIB#7e${nvo@D8naRf@L`CEb{WX(; zO?8;8$D|1B!=O~6{rDFvmZR6e=LsT16)WllqBMJ@^Kk`uC&IOj?@pi*&{s5C%&hjMFH zC{`yF>6C`b5FP2_092;PWr1dm$YC2Clzv&Rpt417txrbf0C-%VD@yagGGF8hmi44h zNflQ66r$le5o*0?AP3;01XrBkN)ntw`5E_<=F5dxV>5<@^uelY8pn z?)1l(bi8hC)@n(3qQR@=MQ9)edxI<|6YV6UGef+mDH?2ywLN(=?E$#gjh@)}WubSJ zzcvXj-P}lX^t(Rcd5iG8jqe-hI|Hv9JB8~l{Lp1?v?(_BgdeyHD@8Y4*O)N)gui-+ z8=Dkb?~+b)O|6Y~mP@}T@NOB?;N88G zI18LaMDrQzaMAk!g;B#CL{v@}W3`_<#^M=X@J|YZfpUnN_qX%IZ3{OCjPi)WRaZ+# zR>7dNR)(e`3W|1GSPj_6ntFwa=h3MtHaYPn^gfM^4SmGoq2~fO`Sxd=yY(U0738`* zp#%J=pC6im(s1qLd`G9y`+L5x{mpboeWnx$*+AN?PS+{v>omJP0TUZ(;vYA^nI6O) zZ10cxN2Al%V{P}jE2I4Hrup`6?)LSV-vezG9*#tVe_-)80 z{qa5IeB-faW88}tzWo~c`H_zSRJb<zWx8Q49PRP>{4L;(p;c0<{pl;mYAk1cTs`np_BT%lG{|+>_KKZR$Z3WPn z=vPW0o}*h#l9Y;ua}_;BgHz-wgWeyR$wm(NMRYt*NFNX(S|l<1}Q&E=P@I`~B@@#Uub>q93Sg;XVhO=FKaBIl`=2x{ z4BdYZxnx-X>5^KJm%6AWz^G!8AP%A9vL%AZ z`$85j-W;s>zH06`6{@5|xo5*gr9tKQ8|IFl2^}cXtWbfVCJ?MmN$shGVGDm)o3-8HKNq$;!zBdyyni(40DEoJMn)0Z^a zIjM`=iR=O~F5_}sZX8(AreN{hfv-Y2CNajjLzhB%bz+84nA{V~!s<|-WhqI8p^|b_ zmky(C8QR73h$#9M3a>v8?sBwC4zw#-wMgtzs>IG1S4Gq&6Xmd&Ow3N)Dcm(?FD5_7 zWFxT!C_aqErZFclx1y!HYaA47qxUdhf&!XJIQMw#vbJXKM(IEN7L}lsACI8D2uaTV E4Yr!H`2YX_ delta 677 zcmXv~OH30{6z!e1^g~;m2X&?0GO0?vP52pG0aLy!VB9PX|L`%tZSO&&e7W1fG1YxdJ|7wG4xw zWGsCY*DduB(0*D{0D{(IYX_eWIdwnXJs8?v`}}LA6dDVar)J24eZ%}WMRk&{Z4#B3 zjFcf&mLm$nxkMZdj3lMV*6QP(>37@HFUwQK^3%nwFCWV@pUU%71H&kVa4d^Nj52AQ zRn813@e!1awPwb3Yc3`yBwEDB==30c$|KF{SOxxvKQOD7)VcoYiKPSY`DK1dd0R*3 zoxG=av$5~H_xA5vL$JNlVDL3n+O!7OHxt-N#eL7`r$Yoz=UUrz$SWLWp%50xeNPLB zd+@IfYlq#H4Mh~3QUVbI9n2&)+rQXU^A%ii<6 zz_=}y#z}gP352gx9@YMNSD^PeO$Cc?5k)B-7e(AbIljoCi$Ny?cDriNvH>$cuFkVw fNTwxKMxD5aYV4=VS+aikq^kqmfAY`_&Z_x8GJL@# 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 index 5fd70bef93fb3385fd7914c41ccb116c683cf047..bbfc492d35ce57d5cb025486ae45d2d4f5bd88f4 100644 GIT binary patch delta 49 zcmaFovchEp8;i;> -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 +2026-06-22 15:22:40 - 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-22 15:22:40 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 15:22:40 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 15:22:40 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 15:22:40 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:176] - ================================================== +2026-06-22 15:22:40 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:177] - lzwcai-mcpskills-generate-reports MCP Server 启动 +2026-06-22 15:22:40 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:178] - ================================================== +2026-06-22 15:22:40 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:179] - 开始运行 MCP Server (stdio 模式) +2026-06-22 15:22:40 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-22 15:22:40 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') +2026-06-22 15:22:40 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 15:22:40 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-22 15:22:40 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-22 15:22:40 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:99] - 收到 ListTools 请求,返回 2 个工具 +2026-06-22 15:22:40 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 15:22:43 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 15:22:44 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 15:22:44 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 15:22:44 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:130] - 收到 CallTool 请求: name=scan_template, arguments={"template": "https://dempminios3.lzwcai.com/lzwcai/upload/2026-06-22/c91dbdfaed46e34ccbe03f9bb9848896/template.docx"} +2026-06-22 15:22:44 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:144] - 工具执行成功: scan_template +2026-06-22 15:22:44 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 15:23:13 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 15:23:13 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 15:23:13 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 15:23:13 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 15:23:34 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 15:23:34 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 15:23:34 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 15:23:34 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:130] - 收到 CallTool 请求: name=generate_report, arguments={"template": "https://dempminios3.lzwcai.com/lzwcai/upload/2026-06-22/c91dbdfaed46e34ccbe03f9bb9848896/template.docx", "data": {"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": "面议"}]}} +2026-06-22 15:23:34 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:144] - 工具执行成功: generate_report +2026-06-22 15:23:34 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 15:27:29 - 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-22 15:27:29 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 15:27:29 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 15:27:29 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 15:28:22 - 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-22 15:28:22 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 15:28:22 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 15:28:22 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 15:28:22 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:175] - ================================================== +2026-06-22 15:28:22 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:176] - lzwcai-mcpskills-generate-reports MCP Server 启动 +2026-06-22 15:28:22 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:177] - ================================================== +2026-06-22 15:28:22 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:178] - 开始运行 MCP Server (stdio 模式) +2026-06-22 15:28:22 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-22 15:28:22 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') +2026-06-22 15:28:24 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 15:28:24 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-22 15:28:24 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-22 15:28:24 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:98] - 收到 ListTools 请求,返回 2 个工具 +2026-06-22 15:28:24 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 15:28:27 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 15:28:27 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 15:28:27 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 15:28:27 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 CallTool 请求: name=scan_template, arguments={"template": "https://dempminios3.lzwcai.com/lzwcai/upload/2026-06-22/c91dbdfaed46e34ccbe03f9bb9848896/template.docx"} +2026-06-22 15:28:27 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:143] - 工具执行成功: scan_template +2026-06-22 15:28:27 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 15:32:56 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 15:32:56 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 15:32:56 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 15:32:56 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 CallTool 请求: name=scan_template, arguments={"template": "https://dempminios3.lzwcai.com/lzwcai/upload/2026-06-22/c91dbdfaed46e34ccbe03f9bb9848896/template.docx"} +2026-06-22 15:32:56 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:143] - 工具执行成功: scan_template +2026-06-22 15:32:56 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 15:34:00 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 15:34:00 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 15:34:00 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 15:34:00 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 CallTool 请求: name=generate_report, arguments={"template": "https://dempminios3.lzwcai.com/lzwcai/upload/2026-06-22/c91dbdfaed46e34ccbe03f9bb9848896/template.docx", "data": {"type": "QuoteData", "project_title": "3C电子产品全自动组装生产线项目报价方案", "contact_person": "王经理", "contact_phone": "13800138000", "contact_company": "深圳恒信电子科技有限公司", "requirements": ["实现手机主板自动上料、焊接、检测、组装一体化生产", "整线产能不低于3000片/小时,兼容3款不同型号主板切换生产", "配备视觉检测系统,不良品自动分流分拣", "提供1年整机质保,7×24小时远程技术支持,上门维修48小时内到场", "交付包含设备操作培训、设备维护手册、完整电气图纸"], "layout_image": "https://demo-image.com/production-line-layout.png", "layout_title": "3C整线布局尺寸效果图", "show_layout": true, "equipments": [{"name": "全自动PCB上料机", "index": "一", "images": ["https://demo-image.com/equip01-1.png", "https://demo-image.com/equip01-2.png"], "features": [{"title": "自动料仓供料", "lines": ["大容量料仓,单次可存放500片PCB基板", "缺料自动声光报警,无需人工实时值守"]}, {"title": "精准定位搬运", "lines": ["伺服电机驱动,重复定位精度±0.02mm", "真空吸盘柔性取料,防止基板刮花变形"]}], "params": [{"k": "适用板材尺寸", "v": "120mm*60mm ~ 240mm*120mm"}, {"k": "整机功率", "v": 2.8}, {"k": "工作电压", "v": "AC220V 50Hz"}]}, {"name": "视觉自动焊接工作站", "index": "二", "images": ["https://demo-image.com/equip02-1.png"], "features": [{"title": "视觉定位焊锡", "lines": ["高清CCD视觉自动识别焊点坐标", "支持多焊点同步恒温焊接,温差控制±3℃"]}], "params": [{"k": "焊接温度范围", "v": "200~450℃可调"}, {"k": "单次焊接耗时", "v": "0.8s/点"}]}, {"name": "成品视觉检测分拣机", "index": "三", "images": [], "features": [{"title": "AI缺陷识别", "lines": ["AI算法识别虚焊、漏焊、元件偏移缺陷", "OK/NG双通道自动分流,数据实时存储"]}], "params": [{"k": "检测精度", "v": "0.01mm"}, {"k": "不良识别率", "v": "99.95%"}]}], "quote_items": [{"name": "全自动PCB上料机", "qty": "1套", "image": "https://demo-image.com/quote-item01.png", "desc": "含伺服输送机构、真空上料模组、料仓支架", "price": "128000元"}, {"name": "视觉自动焊接工作站", "qty": "2套", "image": "https://demo-image.com/quote-item02.png", "desc": "CCD视觉系统、恒温焊锡模组、除尘装置", "price": "265000元/套"}, {"name": "成品视觉检测分拣机", "qty": "1套", "image": "", "desc": "AI视觉检测主机、分流输送线、不良品收纳箱", "price": "196000元"}, {"name": "整线电控集成系统", "qty": "1套", "image": "", "desc": "PLC控制柜、人机交互触摸屏、整线联动程序开发", "price": "86000元"}, {"name": "安装调试及上门培训服务费", "qty": "1项", "image": "", "desc": "现场设备安装、联动调试、操作人员技能培训", "price": "32000元"}], "auto_generated": {"section_quote_table": "", "section_after_sales": ""}}} +2026-06-22 15:34:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:143] - 工具执行成功: generate_report +2026-06-22 15:34:01 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 15:41:12 - 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-22 15:41:12 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 15:41:12 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 15:41:12 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 15:41:46 - 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-22 15:41:46 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 15:41:46 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 15:41:46 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 17:19:53 - 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-22 17:19:53 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 17:19:53 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 17:19:53 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 17:20:36 - 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-22 17:20:36 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 17:20:36 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 17:20:36 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 17:20:36 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-22 17:20:36 - mcp.server.lowlevel.server - DEBUG - [server.py:483] - Tool cache miss for scan_template, refreshing cache +2026-06-22 17:20:36 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:98] - 收到 ListTools 请求,返回 2 个工具 +2026-06-22 17:20:36 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:135] - 收到 CallTool 请求: name=scan_template, arguments=None +2026-06-22 17:20:36 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:149] - 工具执行成功: scan_template +2026-06-22 22:24:48 - 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-22 22:24:48 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 22:24:48 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 22:24:48 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 22:24:48 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:205] - ================================================== +2026-06-22 22:24:48 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:206] - lzwcai-mcpskills-generate-reports MCP Server 启动 +2026-06-22 22:24:48 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:207] - ================================================== +2026-06-22 22:24:48 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:208] - 开始运行 MCP Server (stdio 模式) +2026-06-22 22:24:48 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-22 22:24:48 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') +2026-06-22 22:24:49 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 22:24:49 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-22 22:24:49 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-22 22:24:49 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:121] - 收到 ListTools 请求,返回 2 个工具 +2026-06-22 22:24:49 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 22:25:22 - 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-22 22:25:22 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 22:25:22 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 22:25:22 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 22:25:22 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:205] - ================================================== +2026-06-22 22:25:22 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:206] - lzwcai-mcpskills-generate-reports MCP Server 启动 +2026-06-22 22:25:22 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:207] - ================================================== +2026-06-22 22:25:22 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:208] - 开始运行 MCP Server (stdio 模式) +2026-06-22 22:25:22 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-22 22:25:22 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') +2026-06-22 22:25:23 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 22:25:23 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-22 22:25:23 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-22 22:25:23 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:121] - 收到 ListTools 请求,返回 2 个工具 +2026-06-22 22:25:23 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 22:25:38 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 22:25:38 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-22 22:25:38 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-22 22:25:38 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:121] - 收到 ListTools 请求,返回 2 个工具 +2026-06-22 22:25:38 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 22:28:38 - 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-22 22:28:38 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' +2026-06-22 22:28:38 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest +2026-06-22 22:28:38 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest +2026-06-22 22:28:38 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:197] - ================================================== +2026-06-22 22:28:38 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:198] - lzwcai-mcpskills-generate-reports MCP Server 启动 +2026-06-22 22:28:38 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:199] - ================================================== +2026-06-22 22:28:38 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:200] - 开始运行 MCP Server (stdio 模式) +2026-06-22 22:28:38 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor +2026-06-22 22:28:38 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') +2026-06-22 22:28:39 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 22:28:39 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest +2026-06-22 22:28:39 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest +2026-06-22 22:28:39 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:113] - 收到 ListTools 请求,返回 2 个工具 +2026-06-22 22:28:39 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 22:29:37 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 22:29:37 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 22:29:37 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 22:29:37 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:150] - 收到 CallTool 请求: name=scan_template, arguments={"template": "https://dempminios3.lzwcai.com/lzwcai/upload/2026-06-22/c91dbdfaed46e34ccbe03f9bb9848896/template.docx"} +2026-06-22 22:29:37 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:164] - 工具执行成功: scan_template +2026-06-22 22:29:37 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 22:30:08 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 22:30:08 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 22:30:08 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 22:30:08 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:150] - 收到 CallTool 请求: name=generate_report, arguments={"template": "https://dempminios3.lzwcai.com/lzwcai/upload/2026-06-22/c91dbdfaed46e34ccbe03f9bb9848896/template.docx", "data": "{ \"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\": [ "} +2026-06-22 22:30:08 - lzwcai_mcpskills_generate_reports.server - ERROR - [server.py:166] - 工具执行失败: generate_report: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1019 (char 1018) +Traceback (most recent call last): + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 55, in _load_data + obj = json.loads(s) + ^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\__init__.py", line 346, in loads + return _default_decoder.decode(s) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\decoder.py", line 337, in decode + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\decoder.py", line 355, in raw_decode + raise JSONDecodeError("Expecting value", s, err.value) from None +json.decoder.JSONDecodeError: Expecting value: line 1 column 1019 (char 1018) + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 163, in handle_call_tool + result = await anyio.to_thread.run_sync(handler, args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\to_thread.py", line 63, in run_sync + return await get_async_backend().run_sync_in_worker_thread( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 2502, in run_sync_in_worker_thread + return await future + ^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 986, in run + result = context.run(func, *args) + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 120, in _do_generate_report + return _generate_report( + ^^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 99, in generate_report + data = _load_data(data) + ^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 57, in _load_data + raise ValueError(f"data 看起来是 JSON 内容但解析失败: {e}") +ValueError: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1019 (char 1018) +2026-06-22 22:30:08 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 22:30:32 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 22:30:32 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 22:30:32 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 22:30:32 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:150] - 收到 CallTool 请求: name=generate_report, arguments={"template": "https://dempminios3.lzwcai.com/lzwcai/upload/2026-06-22/c91dbdfaed46e34ccbe03f9bb9848896/template.docx", "data": "{ \"type\": \"QuoteData\", \"project_title\": \"3C电子产品全自动组装生产线项目报价方案\", \"contact_person\": \"王经理\", \"contact_phone\": \"13800138000\", \"contact_company\": \"深圳恒信电子科技有限公司\", \"requirements\": [ \"实现手机主板自动上料、焊接、检测、组装一体化生产\", \"整线产能不低于3000片/小时,兼容3款不同型号主板切换生产\", \"配备视觉检测系统,不良品自动分流分拣\", \"提供1年整机质保,7×24小时远程技术支持,上门维修48小时内到场\", \"交付包含设备操作培训、设备维护手册、完整电气图纸\" ], \"layout_image\": \"https://demo-image.com/production-line-layout.png\", \"layout_title\": \"3C整线布局尺寸效果图\", \"show_layout\": true, \"equipments\": [ { \"name\": \"全自动PCB上料机\", \"index\": \"一\", \"images\": [ \"https://demo-image.com/equip01-1.png\", \"https://demo-image.com/equip01-2.png\" ], \"features\": [ { \"title\": \"自动料仓供料\", \"lines\": [ \"大容量料仓,单次可存放500片PCB基板\", \"缺料自动声光报警,无需人工实时值守\" ] }, "} +2026-06-22 22:30:32 - lzwcai_mcpskills_generate_reports.server - ERROR - [server.py:166] - 工具执行失败: generate_report: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1012 (char 1011) +Traceback (most recent call last): + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 55, in _load_data + obj = json.loads(s) + ^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\__init__.py", line 346, in loads + return _default_decoder.decode(s) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\decoder.py", line 337, in decode + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\decoder.py", line 355, in raw_decode + raise JSONDecodeError("Expecting value", s, err.value) from None +json.decoder.JSONDecodeError: Expecting value: line 1 column 1012 (char 1011) + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 163, in handle_call_tool + result = await anyio.to_thread.run_sync(handler, args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\to_thread.py", line 63, in run_sync + return await get_async_backend().run_sync_in_worker_thread( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 2502, in run_sync_in_worker_thread + return await future + ^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 986, in run + result = context.run(func, *args) + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 120, in _do_generate_report + return _generate_report( + ^^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 99, in generate_report + data = _load_data(data) + ^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 57, in _load_data + raise ValueError(f"data 看起来是 JSON 内容但解析失败: {e}") +ValueError: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1012 (char 1011) +2026-06-22 22:30:32 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +2026-06-22 22:31:01 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: +2026-06-22 22:31:01 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest +2026-06-22 22:31:01 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest +2026-06-22 22:31:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:150] - 收到 CallTool 请求: name=generate_report, arguments={"template": "https://dempminios3.lzwcai.com/lzwcai/upload/2026-06-22/c91dbdfaed46e34ccbe03f9bb9848896/template.docx", "data": "{\n \"type\": \"QuoteData\",\n \"project_title\": \"3C电子产品全自动组装生产线项目报价方案\",\n \"contact_person\": \"王经理\",\n \"contact_phone\": \"13800138000\",\n \"contact_company\": \"深圳恒信电子科技有限公司\",\n \"requirements\": [\n \"实现手机主板自动上料、焊接、检测、组装一体化生产\",\n \"整线产能不低于3000片/小时,兼容3款不同型号主板切换生产\",\n \"配备视觉检测系统,不良品自动分流分拣\",\n \"提供1年整机质保,7×24小时远程技术支持,上门维修48小时内到场\",\n \"交付包含设备操作培训、设备维护手册、完整电气图纸\"\n ],\n \"layout_image\": \"https://demo-image.com/production-line-layout.png\",\n \"layout_title\": \"3C整线布局尺寸效果图\",\n \"show_layout\": true,\n \"equipments\": [\n {\n \"name\": \"全自动PCB上料机\",\n \"index\": \"一\",\n \"images\": [\n \"https://demo-image.com/equip01-1.png\",\n \"https://demo-image.com/equip01-2.png\"\n ],\n \"features\": [\n {\n \"title\": \"自动料仓供料\",\n \"lines\": [\n \"大容量料仓,单次可存放500片PCB基板\",\n \"缺料自动声光报警,无需人工实时值守\"\n ]\n },\n {\n \"title\": \"精准定位搬运\",\n \"lines\": [\n \"伺服电机驱动,重复定位精度±0.02mm\",\n \"真空吸盘柔性取料,防止基板刮花变形\"\n ]\n }\n ],\n \"params\": [\n {\n \"k\": \"适用板材尺寸\",\n \"v\": \"120mm*60mm ~ 240mm*120mm\"\n },\n {\n \"k\": \"整机功率\",\n \"v\": 2.8\n },\n {\n \"k\": \"工作电压\",\n \"v\": \"AC220V 50Hz\"\n }\n ]\n },\n {\n \"name\": \"视觉自动焊接工作站\",\n \"index\": \"二\",\n \"images\": [\n \"https://demo-image.com/equip02-1.png\"\n ],\n \"features\": [\n {\n \"title\": \"视觉定位焊锡\",\n \"lines\": [\n \"高清CCD视觉自动识别焊点坐标\",\n \"支持多焊点同步恒温焊接,温差控制±3℃\"\n ]\n }\n ],\n \"params\": [\n {\n \"k\": \"焊接温度范围\",\n \"v\": \"200~450℃可调\"\n },\n {\n \"k\": \"单次焊接耗时\",\n \"v\": \"0.8s/点\"\n }\n ]\n },\n {\n \"name\": \"成品视觉检测分拣机\",\n \"index\": \"三\",\n \"images\": [\n\n ],\n \"features\": [\n {\n \"title\": \"AI缺陷识别\",\n \"lines\": [\n \"AI算法识别虚焊、漏焊、元件偏移缺陷\",\n \"OK/NG双通道自动分流,数据实时存储\"\n ]\n }\n ],\n \"params\": [\n {\n \"k\": \"检测精度\",\n \"v\": \"0.01mm\"\n },\n {\n \"k\": \"不良识别率\",\n \"v\": \"99.95%\"\n }\n ]\n }\n ],\n \"quote_items\": [\n {\n \"name\": \"全自动PCB上料机\",\n \"qty\": \"1套\",\n \"image\": \"https://demo-image.com/quote-item01.png\",\n \"desc\": \"含伺服输送机构、真空上料模组、料仓支架\",\n \"price\": \"128000元\"\n },\n {\n \"name\": \"视觉自动焊接工作站\",\n \"qty\": \"2套\",\n \"image\": \"https://demo-image.com/quote-item02.png\",\n \"desc\": \"CCD视觉系统、恒温焊锡模组、除尘装置\",\n \"price\": \"265000元/套\"\n },\n {\n \"name\": \"成品视觉检测分拣机\",\n \"qty\": \"1套\",\n \"image\": \"\",\n \"desc\": \"AI视觉检测主机、分流输送线、不良品收纳箱\",\n \"price\": \"196000元\"\n },\n {\n \"name\": \"整线电控集成系统\",\n \"qty\": \"1套\",\n \"image\": \"\",\n \"desc\": \"PLC控制柜、人机交互触摸屏、整线联动程序开发\",\n \"price\": \"86000元\"\n },\n {\n \"name\": \"安装调试及上门培训服务费\",\n \"qty\": \"1项\",\n \"image\": \"\",\n \"desc\": \"现场设备安装、联动调试、操作人员技能培训\",\n \"price\": \"32000元\"\n }\n ],\n \"auto_generated\": {\n \"section_quote_table\": \"\",\n \"section_after_sales\": \"\"\n }\n}"} +2026-06-22 22:31:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:164] - 工具执行成功: generate_report +2026-06-22 22:31:02 - 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 index e69de29..c4e3d84 100644 --- 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 @@ -0,0 +1,78 @@ +2026-06-22 22:30:08 - lzwcai_mcpskills_generate_reports.server - ERROR - [server.py:166] - 工具执行失败: generate_report: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1019 (char 1018) +Traceback (most recent call last): + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 55, in _load_data + obj = json.loads(s) + ^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\__init__.py", line 346, in loads + return _default_decoder.decode(s) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\decoder.py", line 337, in decode + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\decoder.py", line 355, in raw_decode + raise JSONDecodeError("Expecting value", s, err.value) from None +json.decoder.JSONDecodeError: Expecting value: line 1 column 1019 (char 1018) + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 163, in handle_call_tool + result = await anyio.to_thread.run_sync(handler, args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\to_thread.py", line 63, in run_sync + return await get_async_backend().run_sync_in_worker_thread( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 2502, in run_sync_in_worker_thread + return await future + ^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 986, in run + result = context.run(func, *args) + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 120, in _do_generate_report + return _generate_report( + ^^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 99, in generate_report + data = _load_data(data) + ^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 57, in _load_data + raise ValueError(f"data 看起来是 JSON 内容但解析失败: {e}") +ValueError: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1019 (char 1018) +2026-06-22 22:30:32 - lzwcai_mcpskills_generate_reports.server - ERROR - [server.py:166] - 工具执行失败: generate_report: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1012 (char 1011) +Traceback (most recent call last): + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 55, in _load_data + obj = json.loads(s) + ^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\__init__.py", line 346, in loads + return _default_decoder.decode(s) + ^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\decoder.py", line 337, in decode + obj, end = self.raw_decode(s, idx=_w(s, 0).end()) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\json\decoder.py", line 355, in raw_decode + raise JSONDecodeError("Expecting value", s, err.value) from None +json.decoder.JSONDecodeError: Expecting value: line 1 column 1012 (char 1011) + +During handling of the above exception, another exception occurred: + +Traceback (most recent call last): + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 163, in handle_call_tool + result = await anyio.to_thread.run_sync(handler, args) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\to_thread.py", line 63, in run_sync + return await get_async_backend().run_sync_in_worker_thread( + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 2502, in run_sync_in_worker_thread + return await future + ^^^^^^^^^^^^ + File "D:\anaconda3\Lib\site-packages\anyio\_backends\_asyncio.py", line 986, in run + result = context.run(func, *args) + ^^^^^^^^^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\server.py", line 120, in _do_generate_report + return _generate_report( + ^^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 99, in generate_report + data = _load_data(data) + ^^^^^^^^^^^^^^^^ + File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\main.py", line 57, in _load_data + raise ValueError(f"data 看起来是 JSON 内容但解析失败: {e}") +ValueError: data 看起来是 JSON 内容但解析失败: Expecting value: line 1 column 1012 (char 1011) diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/main.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/main.py new file mode 100644 index 0000000..b084b44 --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/main.py @@ -0,0 +1,115 @@ +# -*- coding: utf-8 -*- +""" +main.py:程序化调用入口(非 CLI、非 MCP server)。 + +两点特性: +1. template / data 既可传本地文件路径,也可传 http/https URL(自动下载)。 +2. 输出路径 out 可省略;省略时落到当前目录下的 _out/, + 文件名按 模板名 + 时间戳 自动生成。 + +用法: + from lzwcai_mcpskills_generate_reports.main import generate_report, scan_report + + # data 传 dict + generate_report(template="./模板.docx", data={...}) + # data 传本地 JSON 路径,out 省略 + generate_report(template="./模板.docx", data="data.json") + # template / data 都传 URL + generate_report( + template="https://host/模板.docx", + data="https://host/data.json", + out="_out/a.docx", + ) + # 扫描占位符,支持本地路径或 URL + scan_report(template="https://host/模板.docx") +""" +import json +import os +import sys +import time + +try: + sys.stdout.reconfigure(encoding="utf-8") +except (AttributeError, OSError): + pass + +from . import generate, scan_template +from .utils.fetch import is_url, local_file + + +def _load_data(data): + """把 data 归一化为 dict:支持 dict、JSON 内容字符串、本地 JSON 文件路径、或 JSON 文件 URL。 + + 字符串的判定顺序: + 1. 以 '{' 开头 -> 当作 JSON 内容直接解析; + 2. 否则尝试 JSON 解析(捕获失败则当作路径); + 3. 本地路径 / URL(URL 会下载到临时文件读取,用完即删)。 + """ + if isinstance(data, dict): + return data + if isinstance(data, str): + s = data.strip() + # 1) 看起来就是 JSON 对象内容(路径/URL 不会以 '{' 开头) + if s.startswith("{"): + try: + obj = json.loads(s) + except json.JSONDecodeError as e: + raise ValueError(f"data 看起来是 JSON 内容但解析失败: {e}") + if not isinstance(obj, dict): + raise TypeError("data 为 JSON 内容时必须是对象(dict)") + return obj + # 2) 尝试当作 JSON 字符串解析(捕获失败说明是路径) + try: + obj = json.loads(s) + if isinstance(obj, dict): + return obj + except (json.JSONDecodeError, ValueError): + pass + # 3) 本地路径 / URL + with local_file(data, suffix=".json") as path: + if not is_url(data) and not os.path.isfile(path): + raise FileNotFoundError(f"数据文件不存在: {data}") + with open(path, "r", encoding="utf-8") as f: + return json.load(f) + raise TypeError( + f"data 必须是 dict、JSON 内容/文件路径/URL 字符串,实际类型: {type(data).__name__}" + ) + + +def _default_out_path(template): + """out 省略时的默认输出路径:/_out/<模板名>_<时间戳>.docx。""" + name = os.path.basename(template.split("?")[0]) # 去掉 URL 查询串 + base = os.path.splitext(name)[0] or "report" + stamp = time.strftime("%Y%m%d_%H%M%S") + return os.path.join(os.getcwd(), "_out", f"{base}_{stamp}.docx") + + +def generate_report(template, data, out=None, style_ref=None): + """生成 docx 报告。 + + 参数: + template: 模板 docx 文件路径,或 http/https URL(自动下载) + data: dict、JSON 文件路径,或 JSON 文件 URL(自动下载) + out: 输出 docx 文件路径;省略则用默认路径(_out/ 下按模板名+时间戳命名) + style_ref: 用户样式参考 docx 路径或 URL(可选) + + 返回: + dict: {"output": 输出文件绝对路径} + """ + data = _load_data(data) + if not out: + out = _default_out_path(template) + out_path = generate(data=data, template=template, out_path=out, style_ref=style_ref) + return {"output": out_path} + + +def scan_report(template=None): + """返回 QuoteData 数据契约结构(该传哪些字段、是否必填、嵌套结构)。 + + 参数: + template: 已忽略,保留兼容旧签名。 + + 返回: + dict: 数据契约,见 schema.describe()。 + """ + return scan_template(template) diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/pipeline.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/pipeline.py index 891aeb5..ea325ac 100644 --- a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/pipeline.py +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/pipeline.py @@ -7,10 +7,18 @@ pipeline.py:总入口 — 串起 数据校验 -> 渲染 -> (可选)样式迁 """ import os import json +import contextlib from .schema import validate, normalize from .render_quote import render from .style_transfer import transplant_style +from .utils.fetch import is_url, local_file + + +@contextlib.contextmanager +def _noop(): + """style_ref 为空时占位用的空上下文,产出 None。""" + yield None def _load_data(data): @@ -40,9 +48,9 @@ def generate(data, template, out_path, style_ref=None): 参数: data: QuoteData dict(或 JSON 文件路径字符串) - template: 模板 docx 文件路径 + template: 模板 docx 文件路径,或 http/https URL(自动下载) out_path: 输出 docx 路径 - style_ref: 用户上传的样式参考 docx 路径(可选) + style_ref: 用户上传的样式参考 docx 路径,或 URL(可选,自动下载) 返回: 生成的 docx 绝对路径 @@ -50,10 +58,7 @@ def generate(data, template, out_path, style_ref=None): 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) + raise TypeError(f"template 必须是文件路径或 URL 字符串,实际类型: {type(template).__name__}") # 归一化 + 校验 data = normalize(data) @@ -66,14 +71,21 @@ def generate(data, template, out_path, style_ref=None): 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) + # 把 template / style_ref 统一解析成本地路径(URL 会下载到临时文件,用完即删) + with local_file(template) as template_path, \ + (local_file(style_ref) if style_ref else _noop()) as style_path: + if not is_url(template) and not os.path.isfile(template_path): + raise FileNotFoundError(f"模板文件不存在: {template}") + template_path = os.path.abspath(template_path) - # 可选样式迁移 - if style_ref: - if not os.path.isfile(style_ref): - raise FileNotFoundError(f"样式参考文件不存在: {style_ref}") - transplant_style(out_path, style_ref, out_path) + # 渲染(优先读取同目录 meta.json 作为图片配置;URL 模板无同目录 meta,则为空) + meta = _load_meta_for_template(template_path) + render(data, out_path, template_path, meta=meta) + + # 可选样式迁移 + if style_ref: + if not is_url(style_ref) and not os.path.isfile(style_path): + raise FileNotFoundError(f"样式参考文件不存在: {style_ref}") + transplant_style(out_path, style_path, out_path) return os.path.abspath(out_path) 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 index 691240e..12ddaee 100644 --- a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/render_quote.py +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/render_quote.py @@ -8,7 +8,6 @@ render_quote.py:渲染引擎 — data + 模板路径 -> docx。 """ import copy import os -import ssl import sys import tempfile import urllib.request @@ -22,6 +21,8 @@ except (AttributeError, OSError): from docxtpl import DocxTemplate, InlineImage from docx.shared import Mm +from .utils.fetch import make_ssl_context + def _resolve_image_path(src, tmp_files): """把图片字段值解析为本地文件路径。 @@ -30,9 +31,7 @@ def _resolve_image_path(src, tmp_files): """ def _download(url): try: - ctx = ssl.create_default_context() - ctx.check_hostname = False - ctx.verify_mode = ssl.CERT_NONE + ctx = make_ssl_context() req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) data = urllib.request.urlopen(req, context=ctx, timeout=30).read() ext = os.path.splitext(url.split("?")[0])[1] or ".png" diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/schema.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/schema.py index ebc97da..f455d66 100644 --- a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/schema.py +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/schema.py @@ -64,8 +64,11 @@ def validate(data): 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") + # 用"键是否存在/为空"判断,避免把合法假值(0、False、空串当 v)误判为缺失 + if not p.get("k"): + errors.append(f"equipments[{i}].params[{j}] 缺少 k") + if "v" not in p or p.get("v") is None: + errors.append(f"equipments[{i}].params[{j}] 缺少 v") # quote_items items = data.get("quote_items", []) @@ -121,3 +124,71 @@ def normalize(data): d["section_after_sales"] = _CN_DIGITS[after_sales_idx - 1] if after_sales_idx <= len(_CN_DIGITS) else str(after_sales_idx) return d + + +def describe(): + """返回 QuoteData 数据契约结构(该传哪些字段、是否必填、嵌套结构)。 + + 与 validate / normalize 保持一致,是「调用方该如何组织数据」的权威说明, + 可直接对照 samples/sample_data.json。返回新副本,调用方可安全修改。 + """ + return { + "type": "QuoteData", + "required": ["project_title", "contact_person", "contact_phone", "requirements"], + "fields": { + "project_title": {"type": "string", "required": True, "desc": "项目/方案标题"}, + "contact_person": {"type": "string", "required": True, "desc": "联系人"}, + "contact_phone": {"type": "string", "required": True, "desc": "联系电话"}, + "contact_company": {"type": "string", "required": False, "desc": "客户公司名(模板用到才生效)"}, + "requirements": { + "type": "string | list[string]", + "required": True, + "desc": "客户要求;传列表会自动拼成多行字符串", + }, + "layout_image": {"type": "string", "required": False, "desc": "整线布局图,本地路径或 URL;空串=占位图,None=不显示"}, + "layout_title": {"type": "string", "required": False, "default": DEFAULTS["layout_title"]}, + "show_layout": {"type": "bool", "required": False, "default": DEFAULTS["show_layout"]}, + "equipments": { + "type": "list", + "required": False, + "desc": "设备清单", + "item": { + "name": {"type": "string", "required": True, "desc": "设备名称"}, + "index": {"type": "string", "required": False, "desc": "章节序号,缺省自动按中文数字(四、五…)补全"}, + "images": {"type": "list[string]", "required": False, "desc": "设备图,路径或 URL 列表;缺省为 ['']"}, + "features": { + "type": "list", + "required": False, + "item": { + "title": {"type": "string", "required": True, "desc": "特点标题"}, + "lines": {"type": "list[string]", "required": True, "desc": "特点说明,多行"}, + }, + }, + "params": { + "type": "list", + "required": False, + "item": { + "k": {"type": "string", "required": True, "desc": "参数名"}, + "v": {"type": "string | number", "required": True, "desc": "参数值,允许 0/空串等合法假值"}, + }, + }, + }, + }, + "quote_items": { + "type": "list", + "required": False, + "desc": "报价表条目", + "item": { + "name": {"type": "string", "required": True, "desc": "条目名称"}, + "qty": {"type": "string", "required": False, "desc": "数量,如 '1套'"}, + "image": {"type": "string", "required": False, "desc": "条目图,路径或 URL;缺省为空串"}, + "desc": {"type": "string", "required": False, "desc": "条目说明"}, + "price": {"type": "string", "required": False, "desc": "价格,如 '面议'"}, + }, + }, + }, + "auto_generated": { + "section_quote_table": "报价表章节序号,按设备数量自动计算,无需提供", + "section_after_sales": "售后服务章节序号,按设备数量自动计算,无需提供", + }, + } diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/server.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/server.py index 0368f42..1e17fb1 100644 --- a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/server.py +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/server.py @@ -2,14 +2,12 @@ """ lzwcai-mcpskills-generate-reports MCP Server -把 docx 模板渲染引擎封装成 MCP 工具,提供三个工具: -- generate_report: 数据 + 模板路径 -> 渲染输出 docx -- scan_template: 扫描模板占位符 / for / if 块 -- validate_report_data: 校验数据契约(不渲染) +把 docx 模板渲染引擎封装成 MCP 工具,提供两个工具: +- generate_report: 数据 + 模板路径 -> 渲染输出 docx +- scan_template: 返回 QuoteData 数据契约结构(该传哪些字段、嵌套结构) stdio 模式运行;所有日志走 stderr,stdout 留给 MCP 协议。 """ -import os import json import logging @@ -22,13 +20,15 @@ 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 + from . import scan_template + from .main import generate_report as _generate_report except ImportError: from lzwcai_mcpskills_generate_reports.utils.logger_config import ( setup_system_logging, get_logger, ) - from lzwcai_mcpskills_generate_reports import ( - generate, scan_template, validate, normalize, + from lzwcai_mcpskills_generate_reports import scan_template + from lzwcai_mcpskills_generate_reports.main import ( + generate_report as _generate_report, ) # 初始化日志系统 @@ -39,22 +39,8 @@ logger = get_logger(__name__) 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 文件的路径字符串" +_DATA_DESC = "报价数据:可以是 JSON 对象,或指向 JSON 文件的路径/URL 字符串" TOOL_DEFS = [ types.Tool( @@ -68,56 +54,54 @@ TOOL_DEFS = [ "properties": { "template": { "type": "string", - "description": "模板 docx 文件路径(必填)", + "description": "模板 docx 文件路径,或 http/https URL(会自动下载)(必填)", }, "data": { - "type": ["object", "string"], + "type": "string", "description": _DATA_DESC + "(必填)", }, "out": { "type": "string", - "description": "输出 docx 文件路径(必填)", + "description": "输出 docx 文件路径(可选);省略则落到当前目录 _out/,按 模板名_时间戳.docx 自动命名", }, "style_ref": { "type": "string", - "description": "用户上传的样式参考 docx 路径(可选),会把其 theme/字体套到结果文档", + "description": "用户上传的样式参考 docx 路径或 URL(可选),会把其 theme/字体套到结果文档", }, }, - "required": ["template", "data", "out"], + "required": ["template", "data"], + }, + # 输出契约:成功返回 {"output": 路径};失败返回 {"error":..., "tool_name":...}。 + # MCP 要求 outputSchema 顶层必须是 type:"object",所以 oneOf 只用来约束 + # 两种 required 组合,否则出错时 structuredContent 会过不了校验。 + outputSchema={ + "type": "object", + "properties": { + "output": {"type": "string", "description": "输出 docx 文件绝对路径"}, + "error": {"type": "string", "description": "错误信息"}, + "tool_name": {"type": "string"}, + }, + "oneOf": [ + {"required": ["output"]}, + {"required": ["error", "tool_name"]}, + ], }, ), types.Tool( name="scan_template", description=( - "扫描 docx 模板中的 Jinja2 占位符,返回需要外部提供的顶层变量列表和 for/if 块结构。" - "用于在渲染前了解模板需要哪些数据字段。" + "返回 QuoteData 数据契约结构:该传哪些字段、是否必填、equipments/features/params/" + "quote_items 等嵌套结构,以及自动生成无需提供的字段。用于在渲染前了解数据该如何组织。" ), inputSchema={ "type": "object", "properties": { "template": { "type": "string", - "description": "模板 docx 文件路径(必填)", + "description": "(已忽略,保留兼容)模板路径或 URL;所有模板共用同一数据契约", }, }, - "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"], + "required": [], }, ), ] @@ -132,31 +116,22 @@ async def handle_list_tools() -> list[types.Tool]: # ── 工具实现(同步函数,放线程池执行)────────────────────── 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} + # 复用 main.generate_report:data 支持 URL,out 可省略(落到 _out/ 自动命名) + return _generate_report( + template=arguments["template"], + data=arguments["data"], + out=arguments.get("out"), + style_ref=arguments.get("style_ref"), + ) def _do_scan_template(arguments: dict) -> dict: - return scan_template(arguments["template"]) - - -def _do_validate(arguments: dict) -> dict: - data = _load_data(arguments["data"]) - norm = normalize(data) - errors = validate(norm) - return {"valid": not errors, "errors": errors} + return scan_template(arguments.get("template")) _HANDLERS = { "generate_report": _do_generate_report, "scan_template": _do_scan_template, - "validate_report_data": _do_validate, } @@ -164,8 +139,14 @@ _HANDLERS = { async def handle_call_tool( name: str, arguments: dict | None, -) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]: - """调用工具""" +) -> tuple[list[types.TextContent], dict]: + """调用工具。 + + 返回 (content, structured) 二元组: + - structured: 结构化结果 dict,SDK 自动填入响应的 structuredContent, + 调用方可直接取值,无需再对 content[].text 做 json.loads。 + - content: 序列化后的 TextContent,保留对老客户端的向后兼容。 + """ logger.info( f"收到 CallTool 请求: name={name}, " f"arguments={json.dumps(arguments, ensure_ascii=False) if arguments else 'None'}" @@ -185,12 +166,13 @@ async def handle_call_tool( logger.error(f"工具执行失败: {name}: {e}", exc_info=True) result = {"error": str(e), "tool_name": name} - return [ + content = [ types.TextContent( type="text", text=json.dumps(result, ensure_ascii=False, indent=2), ) ] + return content, result async def run_server(): 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 index fd854b4..ff5ed58 100644 --- a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/template_scanner.py +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/template_scanner.py @@ -1,133 +1,23 @@ # -*- coding: utf-8 -*- """ -template_scanner.py:扫描 docx 模板中的 Jinja2 占位符。 +template_scanner.py:返回 QuoteData 数据契约结构。 -只读模板,不渲染;返回模板里要求外部提供的数据字段清单。 +历史上本模块扫描 docx 模板里的 Jinja2 占位符;现已改为直接返回 +schema.describe() 的数据契约(该传哪些字段、是否必填、嵌套结构), +更直观、可直接对照 samples/sample_data.json。 + +保留 scan_template 这个名字与 template 参数,向后兼容既有调用方。 """ -import re -import zipfile -import xml.etree.ElementTree as ET - -from jinja2 import Environment, meta -from jinja2 import nodes as jinja_nodes +from .schema import describe -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 scan_template(template_path=None): + """返回 QuoteData 数据契约结构。 - -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。 + 参数: + template_path: 兼容旧签名,已忽略(所有模板共用同一数据契约)。 返回: - { - "placeholders": ["project_title", "equipments", ...], # 需要外部提供的最顶层变量 - "blocks": [ - {"type": "for", "iterator": "eq", "variable": "equipments"}, - {"type": "if", "condition": "show_layout"}, - ... - ] - } + dict: 数据契约,见 schema.describe()。 """ - 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, - } + return describe() diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/fetch.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/fetch.py new file mode 100644 index 0000000..73476eb --- /dev/null +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/utils/fetch.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +""" +fetch.py:把远程文件地址(http/https)下载成本地临时文件。 + +用于让 generate / scan_template 既能接收本地路径,也能直接接收一个 URL, +下载后按本地文件处理,用完即删。 +""" +import contextlib +import os +import ssl +import tempfile +import urllib.request + + +def is_url(s): + """判断字符串是否是 http/https URL。""" + return isinstance(s, str) and s.lower().startswith(("http://", "https://")) + + +def make_ssl_context(): + """构造下载用的 SSL 上下文。 + + 默认开启证书校验(安全)。仅当环境变量 LZWCAI_INSECURE_SSL 设为 + 1/true/yes 时才关闭校验,用于内网自签名证书等可信场景。 + """ + ctx = ssl.create_default_context() + if os.environ.get("LZWCAI_INSECURE_SSL", "").lower() in ("1", "true", "yes"): + ctx.check_hostname = False + ctx.verify_mode = ssl.CERT_NONE + return ctx + + +def download_to_temp(url, suffix=".docx"): + """下载 URL 内容到临时文件,返回本地路径。 + + 参数: + url: 远程文件地址(http/https) + suffix: 当 URL 末尾无扩展名时使用的默认后缀 + + 返回: + 本地临时文件绝对路径(调用方负责删除) + """ + ctx = make_ssl_context() + req = urllib.request.Request(url, headers={"User-Agent": "Mozilla/5.0"}) + data = urllib.request.urlopen(req, context=ctx, timeout=60).read() + + ext = os.path.splitext(url.split("?")[0])[1] or suffix + fd, path = tempfile.mkstemp(suffix=ext) + try: + os.write(fd, data) + finally: + os.close(fd) + return path + + +@contextlib.contextmanager +def local_file(path_or_url, suffix=".docx"): + """统一把"本地路径或 URL"解析成本地路径的上下文管理器。 + + - 传入本地路径:原样产出,退出时不删除。 + - 传入 URL:下载到临时文件并产出其路径,退出时自动删除。 + + 用法: + with local_file(template) as path: + ... 用 path 读模板 ... + """ + if is_url(path_or_url): + tmp = download_to_temp(path_or_url, suffix=suffix) + try: + yield tmp + finally: + try: + os.remove(tmp) + except OSError: + pass + else: + yield path_or_url diff --git a/lzwcai_mcpskills_generate_reports/main.py b/lzwcai_mcpskills_generate_reports/main.py index 7946a46..436e90f 100644 --- a/lzwcai_mcpskills_generate_reports/main.py +++ b/lzwcai_mcpskills_generate_reports/main.py @@ -1,9 +1,15 @@ +# -*- coding: utf-8 -*- """ -Entry point for lzwcai-mcpskills-generate-reports +main.py:启动 lzwcai-mcpskills-generate-reports MCP Server (stdio 模式)。 -Runs the MCP server (stdio mode) for docx report generation. +运行: + python main.py + +等价于: + python -m lzwcai_mcpskills_generate_reports.server + +stdio 模式下 stdout 被 MCP 协议占用,所有日志写入 logs/ 目录与 stderr。 """ - from lzwcai_mcpskills_generate_reports.server import main if __name__ == "__main__": diff --git a/lzwcai_mcpskills_generate_reports/pyproject.toml b/lzwcai_mcpskills_generate_reports/pyproject.toml index f63b8aa..a77eebd 100644 --- a/lzwcai_mcpskills_generate_reports/pyproject.toml +++ b/lzwcai_mcpskills_generate_reports/pyproject.toml @@ -4,10 +4,11 @@ build-backend = "setuptools.build_meta" [project] name = "lzwcai-mcpskills-generate-reports" -version = "0.1.0" +version = "0.1.3" description = "Render styled quotation documents from user-supplied docx templates and structured data" readme = "README.md" requires-python = ">=3.12" +license = { text = "MIT" } keywords = ["docx", "quotation", "report", "template", "jinja2"] authors = [ { name = "LzwCai", email = "lzwcai@example.com" }, @@ -27,7 +28,6 @@ dependencies = [ ] [project.scripts] -generate-report = "lzwcai_mcpskills_generate_reports.cli:main" lzwcai-mcpskills-generate-reports = "lzwcai_mcpskills_generate_reports.server:main" [project.urls]