```
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:
@@ -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` - 预览导入数据
|
||||
|
||||
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={},
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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}")
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
1139
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt
Normal file
1139
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/txt
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,10 @@
|
||||
from .env_config import get_api_key, get_base_url, get_env_config
|
||||
from .env_config import (
|
||||
get_api_key,
|
||||
get_base_url,
|
||||
get_env_config,
|
||||
get_account,
|
||||
get_password,
|
||||
)
|
||||
from .logger_config import setup_system_logging, get_logger
|
||||
from .api_client import AgileDBAPIClient, get_default_client
|
||||
|
||||
@@ -6,6 +12,8 @@ __all__ = [
|
||||
'get_api_key',
|
||||
'get_base_url',
|
||||
'get_env_config',
|
||||
'get_account',
|
||||
'get_password',
|
||||
'setup_system_logging',
|
||||
'get_logger',
|
||||
'AgileDBAPIClient',
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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
|
||||
|
||||
@@ -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: 环境变量值
|
||||
|
||||
@@ -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 识别出的、可能与表字段不符的列。
|
||||
|
||||
**返回**:导入结果
|
||||
|
||||
---
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user