diff --git a/.kilo/skills/lzwcai-agile-db.zip b/.kilo/skills/lzwcai-agile-db.zip new file mode 100644 index 0000000..d405ac1 Binary files /dev/null and b/.kilo/skills/lzwcai-agile-db.zip differ diff --git a/.kilo/skills/lzwcai-agile-db/SKILL.md b/.kilo/skills/lzwcai-agile-db/SKILL.md index ac1b2ec..eb75b5e 100644 --- a/.kilo/skills/lzwcai-agile-db/SKILL.md +++ b/.kilo/skills/lzwcai-agile-db/SKILL.md @@ -2,7 +2,7 @@ name: lzwcai-agile-db description: AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据库操作工作流指导,适合零基础用户。 metadata: - version: 0.4.2 + version: 0.4.4 --- # lzwcai-agile-db @@ -97,17 +97,16 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 | `toggle_api_key_status` | 启用/禁用 API 密钥 | 中等 | | `delete_api_key` | 删除 API 密钥 | **危险** | | `get_api_key_permissions` | 查看指定密钥的权限配置 | 安全 | -| `grant_api_key_permissions` | 批量为 API 密钥授予权限 | **危险** | -| `revoke_api_key_permissions` | 撤销/删除已授予的权限(按权限记录 ID) | **危险** | +| `grant_api_key_permissions` | 批量为 API 密钥授予权限(仅追加,不可撤销) | **危险** | ### 九、技能与工具管理(7 个工具) | 工具 | 功能 | 危险等级 | |------|------|----------| +| `add_sql_tool_to_datasource` | 把 SQL 沉淀为工具(一步到位,自动建技能+配模板+建工具,去重幂等)**唯一推荐入口** | 中等 | | `get_skill_by_datasource` | 根据数据源获取技能信息 | 安全 | | `get_skill_tools` | 获取技能下的工具列表 | 安全 | -| `create_skill` | 为数据源创建技能 | 中等 | -| `create_sql_tool` | 将 SQL 查询创建为可复用工具 | 中等 | +| `create_sql_tool` | 将 SQL 创建为可复用工具(底层积木,需技能已存在) | 中等 | | `delete_skill_tool` | 删除技能下的工具 | **危险** | | `update_skill_config` | 更新技能配置(名称/描述/模板) | 中等 | | `update_skill_tool` | 修改技能工具(id+description+uniqueName) | 中等 | @@ -188,16 +187,19 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 当执行以下类型的操作时,**必须先询问用户确认**,不得擅作主张: -1. **删除操作**:删除数据源、删除表数据、删除 API 密钥、删除技能工具等 +1. **数据写操作(增 / 删 / 改)**:插入数据(`insert_table_row`)、更新数据(`update_table_row`)、删除数据(`delete_table_rows`)、导入数据(`confirm_import_data`)等 + - 说明:写操作会改变库中数据,**执行前必须把将要写入/修改/删除的具体内容预览给用户,等待用户明确确认后才执行**;删除不可恢复 + +2. **删除操作**:删除数据源、删除表数据、删除 API 密钥、删除技能工具等 - 说明:此操作不可恢复,数据将永久丢失 -2. **泄密风险操作**:导出包含敏感数据的表、创建 API 密钥、查看密钥详情等 +3. **泄密风险操作**:导出包含敏感数据的表、创建 API 密钥、查看密钥详情等 - 说明:可能导致敏感信息泄露,需确认用户授权 -3. **政治敏感操作**:涉及政治相关数据的查询、修改、删除等 +4. **政治敏感操作**:涉及政治相关数据的查询、修改、删除等 - 说明:可能涉及合规风险,需确认用户意图 -4. **疑似违规内容**:涉及色情、暴力、违法等内容的操作 +5. **疑似违规内容**:涉及色情、暴力、违法等内容的操作 - 说明:可能违反法律法规,必须拒绝执行并告知用户 #### 确认格式 @@ -448,6 +450,8 @@ AgileDB 数据库管理平台的 MCP 技能。为 AI Agent 提供完整的数据 当用户需要修改表中的数据时,使用此流程。 +> 🔒 **写操作统一规则(插入 / 更新 / 删除都适用)**:在调用 `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 5294a1b..e83a7aa 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/server.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/__pycache__/server.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db.log b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/logs/lzwcai_mcp_agile_db.log index 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 db7da02..24bde3f 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/api_keys.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/api_keys.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/data_import.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/data_import.cpython-312.pyc index 0c41287..718148a 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/data_import.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/data_import.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/skills.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/skills.cpython-312.pyc index bda77d2..b27d4f5 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/skills.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/__pycache__/skills.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py index 31eb772..46637fa 100644 --- a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py +++ b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/api_keys.py @@ -1,5 +1,8 @@ """ -API 密钥管理工具(含创建、状态切换、删除、权限查询/授予/撤销) +API 密钥管理工具(含创建、状态切换、删除、权限查询/授予) + +注意:权限模型为「仅追加」——grant_api_key_permissions 只能新增权限,后端不支持撤销/删除 +已授予的权限(真机验证 permission 删除接口返回「不支持当前的调用方式」),故不提供 revoke 工具。 """ from ._base import register_tool, ToolDef @@ -93,7 +96,7 @@ class GetApiKeyPermissionsTool(ToolDef): @register_tool("grant_api_key_permissions") class GrantApiKeyPermissionsTool(ToolDef): name = "grant_api_key_permissions" - description = "批量为 API 密钥授予权限" + description = "批量为 API 密钥授予权限(仅追加,不会覆盖或删除已有权限;后端不支持撤销已授予的权限)" input_schema = { "type": "object", "properties": { @@ -119,34 +122,3 @@ class GrantApiKeyPermissionsTool(ToolDef): async def execute(self, args: dict) -> dict: return await self.client.post("/datasource/api_key/permission/grant_batch", json_data=args) - - -@register_tool("revoke_api_key_permissions") -class RevokeApiKeyPermissionsTool(ToolDef): - name = "revoke_api_key_permissions" - description = "撤销/删除 API 密钥已授予的权限(按权限记录 ID)" - input_schema = { - "type": "object", - "properties": { - "permissionIds": { - "type": "array", - "items": {"type": "string"}, - "description": ( - "权限记录 ID 列表。" - "先从 get_api_key_permissions 获取," - "取 connectionPermissions / databasePermissions / tablePermissions 中每项的 id 字段" - ), - }, - }, - "required": ["permissionIds"], - } - - async def execute(self, args: dict) -> dict: - args = dict(args) - permission_ids = args.pop("permissionIds", None) or [] - # 过滤掉空字符串/None,防止拼接出类似 "1,,2" 的非法 ID - permission_ids = [pid for pid in permission_ids if pid is not None and str(pid).strip()] - if not permission_ids: - raise ValueError("permissionIds 不能为空") - ids = ",".join(str(pid).strip() for pid in permission_ids) - return await self.client.delete(f"/datasource/api_key/permission/{ids}") diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/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 94343c0..fef48fa 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/__init__.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/__init__.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/api_client.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/api_client.cpython-312.pyc index e9fcfa9..278e394 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/api_client.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/api_client.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/env_config.cpython-312.pyc b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/env_config.cpython-312.pyc index 8b1c8c7..5476b98 100644 Binary files a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/env_config.cpython-312.pyc and b/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/__pycache__/env_config.cpython-312.pyc differ diff --git a/lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/utils/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 0fee5cb..32c6ff9 100644 Binary files a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/pipeline.cpython-312.pyc and b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/pipeline.cpython-312.pyc differ diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/render_quote.cpython-312.pyc b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/render_quote.cpython-312.pyc index 6c9bfae..f058445 100644 Binary files a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/render_quote.cpython-312.pyc and b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/render_quote.cpython-312.pyc differ diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/schema.cpython-312.pyc b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/schema.cpython-312.pyc index d257a11..f8bca44 100644 Binary files a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/schema.cpython-312.pyc and b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/schema.cpython-312.pyc differ diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/style_transfer.cpython-312.pyc b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/style_transfer.cpython-312.pyc index 5fd70be..bbfc492 100644 Binary files a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/style_transfer.cpython-312.pyc and b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/__pycache__/style_transfer.cpython-312.pyc differ diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/cli.py b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/cli.py deleted file mode 100644 index ba20c2c..0000000 --- a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/cli.py +++ /dev/null @@ -1,122 +0,0 @@ -# -*- coding: utf-8 -*- -""" -cli.py:公共入口。 - -程序调用(推荐): - from lzwcai_mcpskills_generate_reports.cli import generate_report, scan_report - - generate_report( - template="./模板.docx", - data={"project_name": "x", ...}, - out="_out/a.docx", - ) - scan_report(template="./模板.docx") - -命令行: - generate-report generate --template ./模板.docx --data data.json --out _out/a.docx - generate-report scan --template ./模板.docx -""" -import argparse -import json -import os -import sys - -try: - sys.stdout.reconfigure(encoding="utf-8") -except (AttributeError, OSError): - pass - -from lzwcai_mcpskills_generate_reports import generate, scan_template - - -def _load_data(data): - """把 data 归一化为 dict:支持 dict 或 JSON 文件路径字符串。""" - if isinstance(data, str): - if not os.path.isfile(data): - raise FileNotFoundError(f"数据文件不存在: {data}") - with open(data, "r", encoding="utf-8") as f: - data = json.load(f) - - if not isinstance(data, dict): - raise TypeError(f"data 必须是 dict 或 JSON 文件路径字符串,实际类型: {type(data).__name__}") - return data - - -def generate_report(template, data, out, style_ref=None): - """生成 docx 报告。 - - 参数: - template: 模板 docx 文件路径 - data: dict 数据 或 JSON 文件路径字符串 - out: 输出 docx 文件路径 - style_ref: 用户样式参考 docx 路径(可选) - - 返回: - dict: {"output": 输出文件绝对路径} - """ - data = _load_data(data) - out = generate(data=data, template=template, out_path=out, style_ref=style_ref) - return {"output": out} - - -def scan_report(template): - """扫描模板占位符。 - - 参数: - template: 模板 docx 文件路径 - - 返回: - dict: 占位符扫描结果 - """ - return scan_template(template) - - -def _build_arg_parser(): - parser = argparse.ArgumentParser( - description="docx 模板渲染与占位符扫描", - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=""" -示例: - generate-report generate --template ./模板.docx --data data.json --out _out/a.docx - generate-report generate --template ./模板.docx --data data.json --style-ref 用户样式.docx --out _out/b.docx - generate-report scan --template ./模板.docx - """, - ) - sub = parser.add_subparsers(dest="command", required=True) - - gen_parser = sub.add_parser("generate", help="渲染生成 docx") - gen_parser.add_argument("--template", required=True, help="模板 docx 文件路径") - gen_parser.add_argument("--data", required=True, help="数据 JSON 文件路径") - gen_parser.add_argument("--out", required=True, help="输出 docx 文件路径") - gen_parser.add_argument("--style-ref", default=None, help="用户上传的样式参考 docx(可选)") - - scan_parser = sub.add_parser("scan", help="扫描模板占位符") - scan_parser.add_argument("--template", required=True, help="模板 docx 文件路径") - - return parser - - -def main(): - """命令行入口(console_scripts 调用)。""" - parser = _build_arg_parser() - args = parser.parse_args() - - try: - if args.command == "generate": - result = generate_report( - template=args.template, - data=args.data, - out=args.out, - style_ref=args.style_ref, - ) - print(f"生成成功: {result['output']}") - elif args.command == "scan": - result = scan_report(template=args.template) - print(json.dumps(result, ensure_ascii=False, indent=2)) - except Exception as e: - print(f"错误: {e}", file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/logs/lzwcai_mcpskills_generate_reports.log b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/logs/lzwcai_mcpskills_generate_reports.log index 8ae8aa3..30a3d82 100644 --- a/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/logs/lzwcai_mcpskills_generate_reports.log +++ b/lzwcai_mcpskills_generate_reports/lzwcai_mcpskills_generate_reports/logs/lzwcai_mcpskills_generate_reports.log @@ -1,37 +1,239 @@ -2026-06-16 16:27:35 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs -2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' -2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest -2026-06-16 16:27:35 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest -2026-06-16 16:27:35 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor -2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具 -2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=validate_report_data, arguments={"data": "samples\\sample_data.json"} -2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: validate_report_data -2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=scan_template, arguments={"template": "templates\\standard\\template.docx"} -2026-06-16 16:27:35 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: scan_template -2026-06-16 16:28:01 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs -2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' -2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest -2026-06-16 16:28:01 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest -2026-06-16 16:28:01 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor -2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具 -2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=validate_report_data, arguments={"data": "samples\\sample_data.json"} -2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: validate_report_data -2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=scan_template, arguments={"template": "templates\\standard\\template.docx"} -2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: scan_template -2026-06-16 16:28:01 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:169] - 收到 CallTool 请求: name=generate_report, arguments={"template": "templates\\standard\\template.docx", "data": "samples\\sample_data.json", "out": "_out\\mcp_test.docx"} -2026-06-16 16:28:20 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:183] - 工具执行成功: generate_report -2026-06-17 10:11:02 - root - INFO - [logger_config.py:95] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcpskills_generate_reports\lzwcai_mcpskills_generate_reports\logs -2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcpskills_generate_reports' -2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest -2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest -2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:215] - ================================================== -2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:216] - lzwcai-mcpskills-generate-reports MCP Server 启动 -2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:217] - ================================================== -2026-06-17 10:11:02 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:218] - 开始运行 MCP Server (stdio 模式) -2026-06-17 10:11:02 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor -2026-06-17 10:11:02 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0') -2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: -2026-06-17 10:11:03 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest -2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest -2026-06-17 10:11:03 - lzwcai_mcpskills_generate_reports.server - INFO - [server.py:129] - 收到 ListTools 请求,返回 3 个工具 -2026-06-17 10:11:03 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent +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]