feat(lzwcai-agile-db): 更新版本至0.4.4并优化数据库管理技能文档

- 更新版本号从0.4.2到0.4.4
- 优化API密钥权限管理说明,明确grant_api_key_permissions仅支持追加不支持撤销
- 新增add_sql_tool_to_datasource工具,提供一键创建SQL工具功能
- 调整create_sql_tool说明,强调需技能已存在
- 强化数据写操作安全机制,插入/更新/删除前必须预览并等待用户确认
- 完善导入数据功能说明,详细解释confirm_import_data参数传递方式
- 补充技能与工具管理流程,提供更清晰的操作指引
- 新增数字员工平台数据库技能配置指南文档
```
This commit is contained in:
2026-06-26 16:21:41 +08:00
parent ba5cd4bbe1
commit 635313a7ab
43 changed files with 3464 additions and 686 deletions

View File

@@ -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` - 预览导入数据

View File

@@ -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=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12F4F5C0>
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000020F12F165D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - start_tls.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x0000020F12DFD970>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Mon, 22 Jun 2026 15:02:58 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
2026-06-22 23:02:49 - httpx - INFO - [_client.py:1740] - HTTP Request: POST https://dempdemo.lzwcai.com/api/login "HTTP/1.1 200 "
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-22 23:02:49 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:161] - [API响应] HTTP 200
2026-06-22 23:02:49 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:137] - [登录] 成功获取 token
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
2026-06-22 23:02:49 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
2026-06-23 09:47:20 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
2026-06-23 09:47:20 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
2026-06-23 09:47:20 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-23 09:47:20 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB167DF860>
2026-06-23 09:47:21 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8088, 认证方式=account:yy8z9
2026-06-23 09:47:21 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
2026-06-23 09:47:21 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001FB156EB260>
2026-06-23 09:47:28 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-23 09:47:28 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8088/login, username=yy8z9, loginType=user
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8088 local_address=None timeout=30.0 socket_options=None
2026-06-23 09:47:28 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001FB16D14260>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8088/login "HTTP/1.1 200 "
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8088/datasource/api_key/list
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Date', b'Tue, 23 Jun 2026 01:47:27 GMT'), (b'Keep-Alive', b'timeout=60'), (b'Connection', b'keep-alive')])
2026-06-23 09:47:28 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8088/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 09:47:28 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
2026-06-23 09:47:28 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 21,
"rows": [
{
"createBy": "",
"createTime": "2026-06-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: <mcp.shared.session.RequestResponder object at 0x000001F89A81AAB0>
2026-06-23 09:48:34 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
2026-06-23 09:48:34 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
2026-06-23 09:48:34 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000001F89A273B00>
2026-06-23 09:48:36 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
2026-06-23 09:48:36 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000001F89C2B8F20>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:216] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 01:48:35 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 09:48:36 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 09:48:36 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:160] - [API响应] HTTP 200
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
2026-06-23 09:48:36 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 21,
"rows": [
{
"createBy": "",
"createTime": "2026-06-18 11:29:46",
"updateBy": "",
"updateTime": "2026-06-18 11:59:25",
"remark": null,
"id": "37",
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
"apiKeyName": "盒马超市只读访问密钥",
"enterpriseId": "1932095424144715777",
"status": 0,
"expireTime": "2027-06-18T11:59:25.000+08:00"
},
{
"createBy": "",
"createTime": "2026-06-18 10:54:40",
...
2026-06-23 09:48:36 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
2026-06-23 09:48:39 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
2026-06-23 11:11:10 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:129] - ==================================================
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - lzwcai-mcp-agile-db MCP Server 启动
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:135] - 已注册工具数量: 56
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:136] - ==================================================
2026-06-23 11:11:10 - lzwcai_mcp_agile_db.server - INFO - [server.py:138] - 开始运行 MCP Server (stdio 模式)
2026-06-23 11:11:10 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-23 11:11:10 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCAFE606E0>
2026-06-23 11:11:11 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://192.168.2.236:8082/api, 认证方式=account:yy8z9
2026-06-23 11:11:11 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 56 个工具
2026-06-23 11:11:11 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0FBFB60>
2026-06-23 11:11:14 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-23 11:11:14 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
2026-06-23 11:11:14 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
2026-06-23 11:11:14 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB15974D0>
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:14 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:13 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 11:11:15 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:15 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
2026-06-23 11:11:15 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 21,
"rows": [
{
"createBy": "",
"createTime": "2026-06-18 11:29:46",
"updateBy": "",
"updateTime": "2026-06-18 11:59:25",
"remark": null,
"id": "37",
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
"apiKeyName": "盒马超市只读访问密钥",
"enterpriseId": "1932095424144715777",
"status": 0,
"expireTime": "2027-06-18T11:59:25.000+08:00"
},
{
"createBy": "",
"createTime": "2026-06-18 10:54:40",
...
2026-06-23 11:11:15 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002BCB0EA5D60>
2026-06-23 11:11:27 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:232] - [API请求] GET http://192.168.2.236:8082/api/datasource/api_key/list
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.started host='192.168.2.236' port=8082 local_address=None timeout=30.0 socket_options=None
2026-06-23 11:11:27 - httpcore.connection - DEBUG - [_trace.py:87] - connect_tcp.complete return_value=<httpcore._backends.anyio.AnyIOStream object at 0x000002BCB100F1D0>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=utf-8'), (b'Content-Length', b'51'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN')])
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:239] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:117] - [登录] POST http://192.168.2.236:8082/api/login, username=yy8z9, loginType=user
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'POST']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'POST']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'POST']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json;charset=UTF-8'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: POST http://192.168.2.236:8082/api/login "HTTP/1.1 200 "
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'POST']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:136] - [登录] 成功获取 token
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_headers.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - send_request_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Tue, 23 Jun 2026 03:11:26 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Accept-Encoding'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Content-Encoding', b'gzip')])
2026-06-23 11:11:27 - httpx - INFO - [_client.py:1740] - HTTP Request: GET http://192.168.2.236:8082/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.started request=<Request [b'GET']>
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - receive_response_body.complete
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.started
2026-06-23 11:11:27 - httpcore.http11 - DEBUG - [_trace.py:87] - response_closed.complete
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:176] - [API响应] HTTP 200
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
2026-06-23 11:11:27 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
"total": 21,
"rows": [
{
"createBy": "",
"createTime": "2026-06-18 11:29:46",
"updateBy": "",
"updateTime": "2026-06-18 11:59:25",
"remark": null,
"id": "37",
"apiKey": "9fqiemn5nhWqTWeZoZnEGwlFgZIEPlFHjI3ucEz6fmY",
"apiKeyName": "盒马超市只读访问密钥",
"enterpriseId": "1932095424144715777",
"status": 0,
"expireTime": "2027-06-18T11:59:25.000+08:00"
},
{
"createBy": "",
"createTime": "2026-06-18 10:54:40",
...
2026-06-23 11:11:27 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.started
2026-06-23 11:13:47 - httpcore.connection - DEBUG - [_trace.py:87] - close.complete
2026-06-23 11:37:08 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
2026-06-23 11:37:08 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
2026-06-23 11:37:08 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:314] - [认证] 重新登录后仍返回 401
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - WARNING - [api_client.py:304] - [认证] 收到 401登录过期尝试重新登录后重试一次
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:72] - [客户端初始化] base_url=http://x, 认证方式=account:u
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:134] - [登录] POST http://x/login, username=u, loginType=user
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:154] - [登录] 成功获取 token
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:294] - [API请求] POST http://x/foo
2026-06-23 11:37:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:236] - [API响应] HTTP 200

View File

@@ -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

View File

@@ -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={},

View File

@@ -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}")

View File

@@ -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,

View File

@@ -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 SchemaJSON 字符串或对象)"},
"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 中的 sqlParamsdict 自动序列化为 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/updateOrGetupsert 语义)"
# 真实工具实体字段(已真机探测确认):主键 id、展示名 uniqueName、描述 description、
# SQL 模板 sqlTemplate、结果类型 resultType。后端不认 skillToolId/businessDescription/businessScenario。
description = (
"修改技能下某个工具的名称/描述/SQL等对应后端 tskilltool/updateOrGetupsert 语义)。"
"改展示名传 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": "技能工具 IDget_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} 的技能 IDgetByDatasource 未返回 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,
}

File diff suppressed because it is too large Load Diff

View File

@@ -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',

View File

@@ -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-Typehttpx 会自动设置 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-Typehttpx 会根据 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

View File

@@ -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: 环境变量值

View File

@@ -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 识别出的、可能与表字段不符的列。
**返回**:导入结果
---

View File

@@ -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()

View File

@@ -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"