feat(lzwcai-agile-db): 更新AgileDB技能至v0.4.2版本并扩展工具集
- 将技能版本从0.2.0升级至0.4.2 - 工具数量从33个扩展至57个,新增数据源管理、AI训练、库表关联配置等功能 - 新增MQTT字段关联同步模块(8个工具)和库表关联配置(3个工具) - 添加重要的契约提示和安全确认原则,包括target默认值、alter_table操作限制等 - 修正工具参数说明,如execute_sql的executableSql改为sql,参数结构优化 - 增强安全机制,明确危险操作的用户确认流程和目标资源选择规则 - 更新README.md中的工具数量统计和功能描述
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# lzwcai-mcp-agile-db
|
||||
|
||||
数据库管理平台 MCP Server,提供 33 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。
|
||||
数据库管理平台 MCP Server,提供 34 个工具用于数据库管理、表操作、数据 CRUD、API 密钥管理、技能与工具管理等。
|
||||
|
||||
## 环境变量
|
||||
|
||||
@@ -58,6 +58,7 @@ lzwcai-mcp-agile-db
|
||||
- `delete_api_key` - 删除密钥
|
||||
- `get_api_key_permissions` - 查看密钥权限
|
||||
- `grant_api_key_permissions` - 授予权限
|
||||
- `revoke_api_key_permissions` - 撤销/删除已授予权限
|
||||
|
||||
### 技能与工具管理
|
||||
- `get_skill_by_datasource` - 获取技能信息
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,202 +1,10 @@
|
||||
2026-06-11 09:30:49 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:124] - ==================================================
|
||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
|
||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
|
||||
2026-06-11 09:30:49 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-11 09:30:49 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-11 09:30:49 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||
2026-06-11 09:30:50 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x0000025426747BF0>
|
||||
2026-06-11 09:30:50 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||
2026-06-11 09:30:50 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||
2026-06-11 09:30:50 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||
2026-06-11 09:30:50 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-11 09:33:24 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:124] - ==================================================
|
||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
|
||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
|
||||
2026-06-11 09:33:24 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-11 09:33:24 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-11 09:33:24 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||
2026-06-11 09:33:25 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x0000020155C7D190>
|
||||
2026-06-11 09:33:25 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||
2026-06-11 09:33:25 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||
2026-06-11 09:33:25 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||
2026-06-11 09:33:25 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=https://dempdemo.lzwcai.com
|
||||
2026-06-11 09:33:25 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 33 个工具
|
||||
2026-06-11 09:33:25 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-11 09:33:45 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x0000020155844410>
|
||||
2026-06-11 09:33:45 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-11 09:33:45 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-11 09:33:45 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_datasources
|
||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/connection/list
|
||||
2026-06-11 09:33:46 - httpx - DEBUG - [_config.py:82] - load_ssl_context verify=True cert=None trust_env=True http2=False
|
||||
2026-06-11 09:33:46 - httpx - DEBUG - [_config.py:148] - load_verify_locations cafile='D:\\anaconda3\\Library\\ssl\\cacert.pem'
|
||||
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000020155E9D8E0>
|
||||
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000020155E740D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
||||
2026-06-11 09:33:46 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000020155D4CFE0>
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:33:40 GMT'), (b'Content-Type', b'application/json;charset=utf-8'), (b'Content-Length', b'51'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
||||
2026-06-11 09:33:46 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/connection/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
|
||||
2026-06-11 09:33:46 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
|
||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
|
||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:91] - [API错误] 登录过期,请重新登录
|
||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.server - ERROR - [server.py:96] - 工具执行失败: list_datasources, 错误: 登录过期,请重新登录
|
||||
Traceback (most recent call last):
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\server.py", line 84, in handle_call_tool
|
||||
result = await tool_instance.execute(arguments or {})
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\tools\datasources.py", line 30, in execute
|
||||
return self.client.get("/api/datasource/connection/list", params=params)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 102, in get
|
||||
return self._handle_response(response, url)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 92, in _handle_response
|
||||
raise Exception(error_msg)
|
||||
Exception: 登录过期,请重新登录
|
||||
2026-06-11 09:33:46 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-11 09:39:21 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-11 09:39:21 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-11 09:39:21 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-11 09:39:21 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-11 09:39:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=https://dempdemo.lzwcai.com
|
||||
2026-06-11 09:39:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/connection/list
|
||||
2026-06-11 09:39:21 - httpx - DEBUG - [_config.py:82] - load_ssl_context verify=True cert=None trust_env=True http2=False
|
||||
2026-06-11 09:39:21 - httpx - DEBUG - [_config.py:148] - load_verify_locations cafile='C:\\Users\\HiWin10\\AppData\\Local\\Programs\\Python\\Python312\\Lib\\site-packages\\certifi\\cacert.pem'
|
||||
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000027729C00710>
|
||||
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x0000027729DFE9D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
||||
2026-06-11 09:39:21 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x0000027729C23170>
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:39:15 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
||||
2026-06-11 09:39:21 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/connection/list "HTTP/1.1 200 "
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
|
||||
2026-06-11 09:39:21 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
|
||||
2026-06-11 09:39:21 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
|
||||
2026-06-11 09:39:52 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:124] - ==================================================
|
||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:125] - lzwcai-mcp-agile-db MCP Server 启动
|
||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:130] - 已注册工具数量: 33
|
||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:131] - ==================================================
|
||||
2026-06-11 09:39:52 - lzwcai_mcp_agile_db.server - INFO - [server.py:133] - 开始运行 MCP Server (stdio 模式)
|
||||
2026-06-11 09:39:52 - asyncio - DEBUG - [proactor_events.py:634] - Using proactor: IocpProactor
|
||||
2026-06-11 09:39:52 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: root=InitializedNotification(method='notifications/initialized', params=None, jsonrpc='2.0')
|
||||
2026-06-11 09:39:53 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002B57D06C860>
|
||||
2026-06-11 09:39:53 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type ListToolsRequest
|
||||
2026-06-11 09:39:53 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type ListToolsRequest
|
||||
2026-06-11 09:39:53 - lzwcai_mcp_agile_db.server - INFO - [server.py:42] - 收到 ListTools 请求
|
||||
2026-06-11 09:39:53 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=https://dempdemo.lzwcai.com
|
||||
2026-06-11 09:39:53 - lzwcai_mcp_agile_db.server - INFO - [server.py:56] - ListTools 响应: 返回 33 个工具
|
||||
2026-06-11 09:39:53 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002B57D188A40>
|
||||
2026-06-11 09:40:08 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_datasources
|
||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/connection/list
|
||||
2026-06-11 09:40:08 - httpx - DEBUG - [_config.py:82] - load_ssl_context verify=True cert=None trust_env=True http2=False
|
||||
2026-06-11 09:40:08 - httpx - DEBUG - [_config.py:148] - load_verify_locations cafile='D:\\anaconda3\\Library\\ssl\\cacert.pem'
|
||||
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57D28C560>
|
||||
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x000002B57D2642D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
||||
2026-06-11 09:40:08 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57CF80410>
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:40:02 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
||||
2026-06-11 09:40:08 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/connection/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
|
||||
2026-06-11 09:40:08 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
|
||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
|
||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_datasources
|
||||
2026-06-11 09:40:08 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 14,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-10 16:47:43",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-06-10 16:47:43",
|
||||
"remark": "设备报价管理系统包含设备基础信息、预设方案模板、报价单主表和明细表四个核心数据对象,支持从设备参数管理到整套产线方案配置的完整报价流程,为销售部门提供标准化报价服务,实现快速方案生成和精准成本核算。",
|
||||
"id": "58",
|
||||
"enterpriseId": "1937166012193443842",
|
||||
"deptId": "1171",
|
||||
"userId": "292",
|
||||
"host": "host.docker.internal",
|
||||
"port": 5432,
|
||||
"datasourceName": "HMD产品",
|
||||
"database...
|
||||
2026-06-11 09:40:08 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-11 09:40:55 - mcp.server.lowlevel.server - DEBUG - [server.py:675] - Received message: <mcp.shared.session.RequestResponder object at 0x000002B57D0617C0>
|
||||
2026-06-11 09:40:55 - mcp.server.lowlevel.server - INFO - [server.py:720] - Processing request of type CallToolRequest
|
||||
2026-06-11 09:40:55 - mcp.server.lowlevel.server - DEBUG - [server.py:723] - Dispatching request of type CallToolRequest
|
||||
2026-06-11 09:40:55 - lzwcai_mcp_agile_db.server - INFO - [server.py:66] - 收到 CallTool 请求: name=list_api_keys
|
||||
2026-06-11 09:40:55 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:100] - [API请求] GET https://dempdemo.lzwcai.com/api/datasource/api_key/list
|
||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - close.started
|
||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - close.complete
|
||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.started host='dempdemo.lzwcai.com' port=443 local_address=None timeout=30.0 socket_options=None
|
||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - connect_tcp.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57D2623C0>
|
||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.started ssl_context=<ssl.SSLContext object at 0x000002B57D2642D0> server_hostname='dempdemo.lzwcai.com' timeout=30.0
|
||||
2026-06-11 09:40:55 - httpcore.connection - DEBUG - [_trace.py:47] - start_tls.complete return_value=<httpcore._backends.sync.SyncStream object at 0x000002B57D2626F0>
|
||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.started request=<Request [b'GET']>
|
||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_headers.complete
|
||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.started request=<Request [b'GET']>
|
||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - send_request_body.complete
|
||||
2026-06-11 09:40:55 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.started request=<Request [b'GET']>
|
||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_headers.complete return_value=(b'HTTP/1.1', 200, b'', [(b'Server', b'nginx/1.25.2'), (b'Date', b'Thu, 11 Jun 2026 01:40:49 GMT'), (b'Content-Type', b'application/json'), (b'Transfer-Encoding', b'chunked'), (b'Connection', b'keep-alive'), (b'Vary', b'Origin'), (b'Vary', b'Access-Control-Request-Method'), (b'Vary', b'Access-Control-Request-Headers'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block'), (b'X-Frame-Options', b'SAMEORIGIN'), (b'Strict-Transport-Security', b'max-age=31536000; includeSubDomains'), (b'X-Frame-Options', b'DENY'), (b'X-Content-Type-Options', b'nosniff'), (b'X-XSS-Protection', b'1; mode=block')])
|
||||
2026-06-11 09:40:56 - httpx - INFO - [_client.py:1038] - HTTP Request: GET https://dempdemo.lzwcai.com/api/datasource/api_key/list?pageNum=1&pageSize=20 "HTTP/1.1 200 "
|
||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.started request=<Request [b'GET']>
|
||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - receive_response_body.complete
|
||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.started
|
||||
2026-06-11 09:40:56 - httpcore.http11 - DEBUG - [_trace.py:47] - response_closed.complete
|
||||
2026-06-11 09:40:56 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:74] - [API响应] HTTP 200
|
||||
2026-06-11 09:40:56 - lzwcai_mcp_agile_db.server - INFO - [server.py:86] - 工具执行成功: list_api_keys
|
||||
2026-06-11 09:40:56 - lzwcai_mcp_agile_db.server - DEBUG - [server.py:87] - 工具返回结果: {
|
||||
"total": 6,
|
||||
"rows": [
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-06-06 15:10:31",
|
||||
"updateBy": "",
|
||||
"updateTime": "2026-06-06 15:10:31",
|
||||
"remark": null,
|
||||
"id": "7",
|
||||
"apiKey": "Lb8LgEJ7eBUU8QMifKUJvo9w6YLAotbKJ-w1DKU8ZrU",
|
||||
"apiKeyName": "AWINBEXT",
|
||||
"enterpriseId": "1937166012193443842",
|
||||
"status": 0,
|
||||
"expireTime": "2027-06-06T15:10:32.000+08:00"
|
||||
},
|
||||
{
|
||||
"createBy": "",
|
||||
"createTime": "2026-05-25 14:47:11",
|
||||
"u...
|
||||
2026-06-11 09:40:56 - mcp.server.lowlevel.server - DEBUG - [server.py:790] - Response sent
|
||||
2026-06-17 11:19:35 - root - INFO - [logger_config.py:102] - 日志系统初始化完成 - 日志目录: E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\logs
|
||||
2026-06-17 11:19:35 - mcp.server.lowlevel.server - DEBUG - [server.py:162] - Initializing server 'lzwcai_mcp_agile_db'
|
||||
2026-06-17 11:19:35 - mcp.server.lowlevel.server - DEBUG - [server.py:439] - Registering handler for ListToolsRequest
|
||||
2026-06-17 11:19:35 - mcp.server.lowlevel.server - DEBUG - [server.py:519] - Registering handler for CallToolRequest
|
||||
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:48] - [客户端初始化] base_url=http://x
|
||||
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:78] - [API响应] HTTP 400
|
||||
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:98] - [API错误] HTTP 400, 参数不合法
|
||||
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:78] - [API响应] HTTP 200
|
||||
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - INFO - [api_client.py:78] - [API响应] HTTP 404
|
||||
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:107] - [API错误] HTTP 404, {"path": "/x"}
|
||||
|
||||
@@ -1,15 +1,2 @@
|
||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:91] - [API错误] 登录过期,请重新登录
|
||||
2026-06-11 09:33:46 - lzwcai_mcp_agile_db.server - ERROR - [server.py:96] - 工具执行失败: list_datasources, 错误: 登录过期,请重新登录
|
||||
Traceback (most recent call last):
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\server.py", line 84, in handle_call_tool
|
||||
result = await tool_instance.execute(arguments or {})
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\tools\datasources.py", line 30, in execute
|
||||
return self.client.get("/api/datasource/connection/list", params=params)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 102, in get
|
||||
return self._handle_response(response, url)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
File "E:\yh-ai\project\lzwcai-szyg\lzwcai-mcp-server-package\lzwcai_mcp_agile_db\lzwcai_mcp_agile_db\utils\api_client.py", line 92, in _handle_response
|
||||
raise Exception(error_msg)
|
||||
Exception: 登录过期,请重新登录
|
||||
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:98] - [API错误] HTTP 400, 参数不合法
|
||||
2026-06-17 11:19:35 - lzwcai_mcp_agile_db.utils.api_client - ERROR - [api_client.py:107] - [API错误] HTTP 404, {"path": "/x"}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
lzwcai-mcp-agile-db MCP Server
|
||||
数据库管理平台 MCP 工具服务,提供 33 个工具用于数据库管理、表操作、API 密钥管理等
|
||||
数据库管理平台 MCP 工具服务,提供数据库管理、表/数据操作、技能、API 密钥、MQTT 同步、AI 训练等工具
|
||||
"""
|
||||
|
||||
import json
|
||||
@@ -104,19 +104,24 @@ async def handle_call_tool(
|
||||
|
||||
async def run_server():
|
||||
"""运行 MCP Server (stdio 模式)"""
|
||||
async with stdio_server() as streams:
|
||||
await server.run(
|
||||
streams[0],
|
||||
streams[1],
|
||||
InitializationOptions(
|
||||
server_name="lzwcai_mcp_agile_db",
|
||||
server_version="0.1.0",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
try:
|
||||
async with stdio_server() as streams:
|
||||
await server.run(
|
||||
streams[0],
|
||||
streams[1],
|
||||
InitializationOptions(
|
||||
server_name="lzwcai_mcp_agile_db",
|
||||
server_version="0.1.12",
|
||||
capabilities=server.get_capabilities(
|
||||
notification_options=NotificationOptions(),
|
||||
experimental_capabilities={},
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
finally:
|
||||
# 释放全局 HTTP 客户端
|
||||
if _api_client is not None:
|
||||
await _api_client.close()
|
||||
|
||||
|
||||
def main():
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
68
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/ai_training.py
Normal file
68
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/ai_training.py
Normal file
@@ -0,0 +1,68 @@
|
||||
"""
|
||||
AI 补全训练工具
|
||||
|
||||
含 generate_table_by_description 的轮询配套 get_ai_training_detail。
|
||||
训练状态 trainingStatus: 0 待训练 / 1 训练中 / 2 已完成 / 3 失败
|
||||
"""
|
||||
|
||||
from ._base import register_tool, ToolDef
|
||||
|
||||
|
||||
@register_tool("list_ai_trainings")
|
||||
class ListAiTrainingsTool(ToolDef):
|
||||
name = "list_ai_trainings"
|
||||
description = "获取 AI 补全训练任务列表"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceId": {"type": "string", "description": "数据源 ID(可选过滤)"},
|
||||
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||
},
|
||||
"required": [],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
params = {k: v for k, v in args.items() if v is not None}
|
||||
return await self.client.get("/ai/training/list", params=params)
|
||||
|
||||
|
||||
@register_tool("create_ai_training_by_selected")
|
||||
class CreateAiTrainingBySelectedTool(ToolDef):
|
||||
name = "create_ai_training_by_selected"
|
||||
description = "按选中的表创建 AI 补全训练任务"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||
"tableIds": {
|
||||
"type": "array",
|
||||
"description": "要训练的表 ID 数组",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"required": ["tableIds"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.post("/ai/training/createBySelected", json_data=args)
|
||||
|
||||
|
||||
@register_tool("get_ai_training_detail")
|
||||
class GetAiTrainingDetailTool(ToolDef):
|
||||
name = "get_ai_training_detail"
|
||||
description = (
|
||||
"查询 AI 训练任务详情(轮询用)。"
|
||||
"trainingStatus: 0 待训练 / 1 训练中 / 2 已完成 / 3 失败。"
|
||||
"generate_table_by_description 返回 taskId 后,用本工具轮询直到 status=2 取结果"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"taskId": {"type": "string", "description": "训练任务 ID(generate_table 返回的 taskId)"},
|
||||
},
|
||||
"required": ["taskId"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.get(f"/ai/training/{args['taskId']}")
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
API 密钥管理工具 (工具 18-23)
|
||||
API 密钥管理工具(含创建、状态切换、删除、权限查询/授予/撤销)
|
||||
"""
|
||||
|
||||
from ._base import register_tool, ToolDef
|
||||
@@ -119,3 +119,34 @@ class GrantApiKeyPermissionsTool(ToolDef):
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.post("/datasource/api_key/permission/grant_batch", json_data=args)
|
||||
|
||||
|
||||
@register_tool("revoke_api_key_permissions")
|
||||
class RevokeApiKeyPermissionsTool(ToolDef):
|
||||
name = "revoke_api_key_permissions"
|
||||
description = "撤销/删除 API 密钥已授予的权限(按权限记录 ID)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"permissionIds": {
|
||||
"type": "array",
|
||||
"items": {"type": "string"},
|
||||
"description": (
|
||||
"权限记录 ID 列表。"
|
||||
"先从 get_api_key_permissions 获取,"
|
||||
"取 connectionPermissions / databasePermissions / tablePermissions 中每项的 id 字段"
|
||||
),
|
||||
},
|
||||
},
|
||||
"required": ["permissionIds"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
permission_ids = args.pop("permissionIds", None) or []
|
||||
# 过滤掉空字符串/None,防止拼接出类似 "1,,2" 的非法 ID
|
||||
permission_ids = [pid for pid in permission_ids if pid is not None and str(pid).strip()]
|
||||
if not permission_ids:
|
||||
raise ValueError("permissionIds 不能为空")
|
||||
ids = ",".join(str(pid).strip() for pid in permission_ids)
|
||||
return await self.client.delete(f"/datasource/api_key/permission/{ids}")
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
"""
|
||||
库表关联配置工具(外置/内置通用)
|
||||
|
||||
对应文档 §2.2 / §4.2:
|
||||
- postConnectionConfig 建库表关联配置(外置源建完连接后选库选表的最后一步)
|
||||
- putConnectionConfig 改库表关联配置
|
||||
- deleteConnectionConfig 删配置(同时也是「删除库」的实现)
|
||||
"""
|
||||
|
||||
from ._base import register_tool, ToolDef
|
||||
|
||||
|
||||
@register_tool("create_connection_config")
|
||||
class CreateConnectionConfigTool(ToolDef):
|
||||
name = "create_connection_config"
|
||||
description = (
|
||||
"创建「库表关联」配置:外置数据源建完连接后,选择要纳管的库与表的最后一步。"
|
||||
"完成后数据源才能用于智能问数"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源(连接)ID"},
|
||||
"syncTables": {"type": "boolean", "default": True, "description": "是否同步表结构,默认 true"},
|
||||
"databases": {
|
||||
"type": "array",
|
||||
"description": "要纳管的库及其表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"databaseName": {"type": "string", "description": "库名"},
|
||||
"tableNames": {
|
||||
"type": "array",
|
||||
"description": "选中的表名数组",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"required": ["databaseName", "tableNames"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["connectionId", "databases"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
args.setdefault("syncTables", True)
|
||||
return await self.client.post("/datasource/config", json_data=args)
|
||||
|
||||
|
||||
@register_tool("update_connection_config")
|
||||
class UpdateConnectionConfigTool(ToolDef):
|
||||
name = "update_connection_config"
|
||||
description = "更新「库表关联」配置(编辑已纳管的库表范围)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "配置 ID(编辑时必传)"},
|
||||
"connectionId": {"type": "string", "description": "数据源(连接)ID"},
|
||||
"syncTables": {"type": "boolean", "default": True, "description": "是否同步表结构,默认 true"},
|
||||
"databases": {
|
||||
"type": "array",
|
||||
"description": "要纳管的库及其表",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"databaseName": {"type": "string", "description": "库名"},
|
||||
"tableNames": {
|
||||
"type": "array",
|
||||
"description": "选中的表名数组",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"required": ["databaseName", "tableNames"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["id", "connectionId", "databases"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
args.setdefault("syncTables", True)
|
||||
return await self.client.put("/datasource/config", json_data=args)
|
||||
|
||||
|
||||
@register_tool("delete_connection_config")
|
||||
class DeleteConnectionConfigTool(ToolDef):
|
||||
name = "delete_connection_config"
|
||||
description = "删除库表关联配置(批量)。这也是「删除内置库」的实现:传入对应库的配置 ID"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {
|
||||
"type": "array",
|
||||
"description": "配置 ID 数组",
|
||||
"items": {"type": "string"},
|
||||
},
|
||||
},
|
||||
"required": ["ids"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.post("/datasource/config/deletes", json_data={"ids": args["ids"]})
|
||||
@@ -2,8 +2,10 @@
|
||||
数据导入工具 (工具 30-31)
|
||||
"""
|
||||
|
||||
import base64
|
||||
import io
|
||||
import mimetypes
|
||||
import os
|
||||
from urllib.parse import urlparse, unquote
|
||||
|
||||
from ._base import register_tool, ToolDef
|
||||
|
||||
@@ -11,62 +13,186 @@ from ._base import register_tool, ToolDef
|
||||
@register_tool("preview_import_data")
|
||||
class PreviewImportDataTool(ToolDef):
|
||||
name = "preview_import_data"
|
||||
description = "上传 Excel 文件,AI 智能识别并预览表结构/数据"
|
||||
description = "从 URL 下载 Excel 文件,AI 智能识别并预览表结构/数据"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源 ID"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"},
|
||||
"file_base64": {"type": "string", "description": "Excel 文件 base64 编码(.xlsx/.xls, <500KB)"},
|
||||
"file_name": {"type": "string", "description": "文件名(如 data.xlsx)"},
|
||||
"file_url": {"type": "string", "description": "Excel 文件下载地址(.xlsx/.xls, <500KB)"},
|
||||
},
|
||||
"required": ["connectionId", "file_base64"],
|
||||
"required": ["connectionId", "file_url"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
target = args.pop("target", "test")
|
||||
file_base64 = args.pop("file_base64")
|
||||
file_name = args.pop("file_name", "import.xlsx")
|
||||
|
||||
# 解码 base64 文件
|
||||
file_content = base64.b64decode(file_base64)
|
||||
|
||||
# 构建文件上传
|
||||
file_url = args.pop("file_url")
|
||||
|
||||
# 0. 基础 URL 校验
|
||||
parsed_url = urlparse(file_url)
|
||||
if parsed_url.scheme not in ("http", "https"):
|
||||
raise ValueError("仅支持 http/https 协议的下载地址")
|
||||
|
||||
# 1. 下载文件
|
||||
download_resp = await self.client.client.get(file_url, follow_redirects=True, timeout=30.0)
|
||||
download_resp.raise_for_status()
|
||||
|
||||
# 1.1 预先根据 Content-Length 拦截超大文件,避免无意义下载
|
||||
max_size = 500 * 1024
|
||||
content_length = download_resp.headers.get("content-length")
|
||||
if content_length:
|
||||
try:
|
||||
if int(content_length) > max_size:
|
||||
raise ValueError(
|
||||
f"文件 Content-Length {content_length} bytes 超过平台限制 {max_size} bytes (500KB)"
|
||||
)
|
||||
except ValueError as e:
|
||||
if "超过平台限制" in str(e):
|
||||
raise
|
||||
# 非数字时忽略
|
||||
|
||||
file_content = download_resp.content
|
||||
|
||||
# 2. 文件大小校验(平台限制 500KB)
|
||||
if len(file_content) > max_size:
|
||||
raise ValueError(
|
||||
f"文件大小 {len(file_content)} bytes 超过平台限制 {max_size} bytes (500KB),"
|
||||
"请先压缩或拆分后重试"
|
||||
)
|
||||
|
||||
# 3. 解析文件名并校验扩展名
|
||||
file_name = self._extract_file_name(download_resp, file_url)
|
||||
allowed_exts = (".xlsx", ".xls")
|
||||
if not file_name.lower().endswith(allowed_exts):
|
||||
raise ValueError(
|
||||
f"不支持的文件类型: {file_name},仅支持 {', '.join(allowed_exts)}"
|
||||
)
|
||||
|
||||
# 4. 推断 MIME 类型
|
||||
content_type, _ = mimetypes.guess_type(file_name)
|
||||
if not content_type:
|
||||
content_type = (
|
||||
"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"
|
||||
if file_name.lower().endswith(".xlsx")
|
||||
else "application/vnd.ms-excel"
|
||||
)
|
||||
|
||||
# 5. 构建文件上传
|
||||
files = {
|
||||
"file": (file_name, io.BytesIO(file_content), "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"),
|
||||
"file": (file_name, io.BytesIO(file_content), content_type),
|
||||
}
|
||||
|
||||
|
||||
return await self.client.upload(
|
||||
f"/datasource/connection/{connection_id}/import_document/preview",
|
||||
files=files,
|
||||
params={"target": target},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _extract_file_name(response, file_url: str) -> str:
|
||||
"""从响应头或 URL 中提取文件名。"""
|
||||
# 优先从 Content-Disposition 解析
|
||||
content_disposition = response.headers.get("content-disposition")
|
||||
if content_disposition:
|
||||
# 简单解析 filename="xxx" 或 filename*=UTF-8''xxx
|
||||
# 注意:filename* 也包含 filename= 前缀,必须先检查 filename*
|
||||
for part in content_disposition.split(";"):
|
||||
part = part.strip()
|
||||
if part.lower().startswith("filename*="):
|
||||
encoded = part.split("=", 1)[1]
|
||||
# 形如 UTF-8''xxx
|
||||
if "''" in encoded:
|
||||
_, _, file_name = encoded.partition("''")
|
||||
return os.path.basename(unquote(file_name, encoding="utf-8"))
|
||||
return os.path.basename(unquote(encoded, encoding="utf-8"))
|
||||
if part.lower().startswith("filename="):
|
||||
file_name = part.split("=", 1)[1].strip('"')
|
||||
return os.path.basename(unquote(file_name))
|
||||
|
||||
# fallback 从 URL 路径获取
|
||||
parsed = urlparse(file_url)
|
||||
path = unquote(parsed.path)
|
||||
file_name = os.path.basename(path) if path else ""
|
||||
if file_name and "." in file_name:
|
||||
return file_name
|
||||
|
||||
return "import.xlsx"
|
||||
|
||||
|
||||
@register_tool("confirm_import_data")
|
||||
class ConfirmImportDataTool(ToolDef):
|
||||
name = "confirm_import_data"
|
||||
description = "确认导入 AI 识别后的数据"
|
||||
description = (
|
||||
"确认导入 AI 识别后的数据(建表+插数据)。"
|
||||
"传入 preview_import_data 返回的 data 原文 + databaseName 即可,"
|
||||
"工具内部会自动组装成后端要求的 {tableStructure(含databaseName), allData} 结构"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源 ID"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "description": "环境"},
|
||||
"data": {"type": "object", "description": "导入数据(含 tableStructure + allData)"},
|
||||
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||
"databaseName": {"type": "string", "description": "落库的数据库名(导入目标库)"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test"},
|
||||
"data": {"type": "object", "description": "preview_import_data 返回的 data(含 tableStructure/allData),或已组装好的最终结构"},
|
||||
},
|
||||
"required": ["connectionId", "data"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _build_body(data: dict, database_name) -> dict:
|
||||
"""把 preview 返回的 data 组装成 confirm 要求的 body。
|
||||
|
||||
后端真实结构(已真机探测确认):
|
||||
{ "tableStructure": <单表对象,含 columns 且需带 databaseName>, "allData": [...] }
|
||||
preview 返回的 data.tableStructure 是 {success,message,data:{tables:[...]}} 的包装,
|
||||
需取出 data.tables[0] 作为单表对象。
|
||||
"""
|
||||
if not isinstance(data, dict):
|
||||
return data
|
||||
|
||||
# 若传入的是 preview 工具的完整返回信封 {code, msg, data:{...}},先剥一层
|
||||
if "tableStructure" not in data and isinstance(data.get("data"), dict):
|
||||
inner_env = data["data"]
|
||||
if "tableStructure" in inner_env or "allData" in inner_env:
|
||||
data = inner_env
|
||||
|
||||
ts = data.get("tableStructure")
|
||||
single_table = None
|
||||
if isinstance(ts, dict):
|
||||
if "columns" in ts:
|
||||
# 已是单表对象(调用方自行组装过)
|
||||
single_table = dict(ts)
|
||||
else:
|
||||
# preview 包装:tableStructure.data.tables[0]
|
||||
inner = ts.get("data") if isinstance(ts.get("data"), dict) else {}
|
||||
tables = inner.get("tables") if isinstance(inner, dict) else None
|
||||
if isinstance(tables, list) and tables:
|
||||
single_table = dict(tables[0])
|
||||
|
||||
if single_table is None:
|
||||
# 无法识别结构,原样透传(兼容调用方已构造好完整 body 的情况)
|
||||
return data
|
||||
|
||||
if database_name and not single_table.get("databaseName"):
|
||||
single_table["databaseName"] = database_name
|
||||
|
||||
all_data = data.get("allData")
|
||||
if all_data is None:
|
||||
all_data = data.get("data") or []
|
||||
return {"tableStructure": single_table, "allData": all_data}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
target = args.pop("target", "test")
|
||||
database_name = args.pop("databaseName", None)
|
||||
data = args.pop("data")
|
||||
|
||||
|
||||
body = self._build_body(data, database_name)
|
||||
return await self.client.post(
|
||||
f"/datasource/connection/{connection_id}/import_document/confirm",
|
||||
json_data=data,
|
||||
json_data=body,
|
||||
params={"target": target},
|
||||
)
|
||||
|
||||
@@ -45,6 +45,28 @@ class ListTablesTool(ToolDef):
|
||||
return await self.client.get("/datasource/table/list", params=params)
|
||||
|
||||
|
||||
@register_tool("list_tables_with_ai")
|
||||
class ListTablesWithAiTool(ToolDef):
|
||||
name = "list_tables_with_ai"
|
||||
description = (
|
||||
"获取表元数据列表(含 AI 训练描述),内置库管理的主列表。"
|
||||
"注意:datasourceId 实为「配置/库」ID(即 create_connection_config 落库后的 config id)"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceId": {"type": "string", "description": "配置/库 ID(非数据源连接 ID)"},
|
||||
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||
},
|
||||
"required": ["datasourceId"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
params = {k: v for k, v in args.items() if v is not None}
|
||||
return await self.client.get("/datasource/table/ailist", params=params)
|
||||
|
||||
|
||||
@register_tool("get_table_detail")
|
||||
class GetTableDetailTool(ToolDef):
|
||||
name = "get_table_detail"
|
||||
@@ -117,8 +139,8 @@ class AlterTableTool(ToolDef):
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"operation": {"type": "string", "enum": ["ADD_COLUMN", "DROP_COLUMN", "RENAME_COLUMN", "ALTER_COLUMN_TYPE", "SET_NOT_NULL", "DROP_NOT_NULL", "SET_DEFAULT", "DROP_DEFAULT"], "description": "变更类型"},
|
||||
"column": {"type": "object", "description": "列定义(根据 operation 不同包含不同字段)"},
|
||||
"operation": {"type": "string", "enum": ["ADD_COLUMN", "MODIFY_COLUMN", "DROP_COLUMN"], "description": "变更类型:新增/修改/删除字段(后端实测仅支持这三种)"},
|
||||
"column": {"type": "object", "description": "列定义。ADD/MODIFY 需 columnName+columnType(+columnLength/columnComment 等);DROP 仅需 columnName"},
|
||||
},
|
||||
"required": ["operation", "column"],
|
||||
},
|
||||
@@ -137,7 +159,7 @@ class AlterTableTool(ToolDef):
|
||||
@register_tool("generate_table_by_description")
|
||||
class GenerateTableByDescriptionTool(ToolDef):
|
||||
name = "generate_table_by_description"
|
||||
description = "通过自然语言描述让 AI 生成表结构(异步任务)"
|
||||
description = "通过自然语言描述让 AI 生成表结构(异步任务,返回 taskId)。需配合 get_ai_training_detail 轮询 taskId 获取生成结果"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
@@ -149,3 +171,116 @@ class GenerateTableByDescriptionTool(ToolDef):
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.post("/datasource/connection/generate_table", json_data=args)
|
||||
|
||||
|
||||
@register_tool("create_database")
|
||||
class CreateDatabaseTool(ToolDef):
|
||||
name = "create_database"
|
||||
description = "在内置数据源下创建一个数据库(建表前置步骤)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||
"databaseName": {"type": "string", "description": "数据库名(仅字母数字下划线)"},
|
||||
},
|
||||
"required": ["connectionId", "databaseName"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
return await self.client.post(f"/datasource/connection/{connection_id}/create_database", json_data=args)
|
||||
|
||||
|
||||
@register_tool("create_database_table")
|
||||
class CreateDatabaseTableTool(ToolDef):
|
||||
name = "create_database_table"
|
||||
description = (
|
||||
"一次性创建数据库 + 表(合并接口,免去先建库再建表)。"
|
||||
"支持两种用法:①单表便捷参数 tableName/tableComment/columns;"
|
||||
"②批量传 tables 数组(一次建多张表)。二者任选其一"
|
||||
)
|
||||
# 单字段(columnName/columnType/...)的 schema,单表和批量两种用法共用
|
||||
_COLUMN_ITEMS = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"columnName": {"type": "string", "description": "字段名"},
|
||||
"columnType": {"type": "string", "description": "字段类型(VARCHAR/INTEGER/SERIAL等)"},
|
||||
"columnLength": {"type": "integer", "description": "字段长度"},
|
||||
"isPrimaryKey": {"type": "boolean", "description": "是否主键"},
|
||||
"isNullable": {"type": "boolean", "description": "是否可空"},
|
||||
"isAutoIncrement": {"type": "boolean", "description": "是否自增"},
|
||||
"columnComment": {"type": "string", "description": "字段注释"},
|
||||
"defaultValue": {"type": "string", "description": "默认值"},
|
||||
},
|
||||
"required": ["columnName", "columnType"],
|
||||
}
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||
"databaseName": {"type": "string", "description": "数据库名(仅字母数字下划线)"},
|
||||
# —— 用法①:单表便捷参数 ——
|
||||
"tableName": {"type": "string", "description": "表名(用法①单表,小写字母+数字+下划线)"},
|
||||
"tableComment": {"type": "string", "description": "表注释(用法①单表)"},
|
||||
"columns": {
|
||||
"type": "array",
|
||||
"description": "字段定义数组(用法①单表,结构同 create_table)",
|
||||
"items": _COLUMN_ITEMS,
|
||||
},
|
||||
# —— 用法②:批量 tables 数组 ——
|
||||
"tables": {
|
||||
"type": "array",
|
||||
"description": "表定义数组(用法②批量建多表),每项含 tableName/tableComment/columns",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableName": {"type": "string", "description": "表名"},
|
||||
"tableComment": {"type": "string", "description": "表注释"},
|
||||
"columns": {"type": "array", "description": "字段定义数组", "items": _COLUMN_ITEMS},
|
||||
},
|
||||
"required": ["tableName", "columns"],
|
||||
},
|
||||
},
|
||||
},
|
||||
"required": ["connectionId", "databaseName"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
# 后端真实 body 结构:{databaseName, tables:[{tableName,tableComment,columns}]}(已真机探测确认)
|
||||
if not args.get("tables"):
|
||||
table = {"tableName": args.pop("tableName", None), "columns": args.pop("columns", [])}
|
||||
if args.get("tableComment") is not None:
|
||||
table["tableComment"] = args.pop("tableComment")
|
||||
args["tables"] = [table]
|
||||
else:
|
||||
# 批量用法下,清掉可能并存的单表字段,避免歧义
|
||||
args.pop("tableName", None)
|
||||
args.pop("tableComment", None)
|
||||
args.pop("columns", None)
|
||||
return await self.client.post(f"/datasource/connection/{connection_id}/create_database_table", json_data=args)
|
||||
|
||||
|
||||
@register_tool("alter_database")
|
||||
class AlterDatabaseTool(ToolDef):
|
||||
name = "alter_database"
|
||||
description = "修改数据库(如改名)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"connectionId": {"type": "string", "description": "数据源连接 ID"},
|
||||
"databaseName": {"type": "string", "description": "当前数据库名"},
|
||||
"newName": {"type": "string", "description": "新数据库名"},
|
||||
},
|
||||
"required": ["connectionId", "databaseName", "newName"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
connection_id = args.pop("connectionId")
|
||||
# 后端改名字段名为 newName(已真机探测确认);兼容历史传参 newDatabaseName
|
||||
if "newName" not in args and "newDatabaseName" in args:
|
||||
args["newName"] = args.pop("newDatabaseName")
|
||||
return await self.client.put(f"/datasource/connection/{connection_id}/alter_database", json_data=args)
|
||||
|
||||
@@ -71,7 +71,7 @@ class CreateDatasourceTool(ToolDef):
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceName": {"type": "string", "description": "数据源名称(3-20字)"},
|
||||
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng"], "description": "数据库类型"},
|
||||
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
|
||||
"host": {"type": "string", "description": "数据库地址"},
|
||||
"port": {"type": "integer", "description": "端口号"},
|
||||
"databaseName": {"type": "string", "description": "要连接的数据库名"},
|
||||
@@ -88,7 +88,7 @@ class CreateDatasourceTool(ToolDef):
|
||||
args = dict(args)
|
||||
test_first = args.pop("test_first", True)
|
||||
|
||||
# 如果需要先测试连接
|
||||
# 如果需要先测试连接(测试失败时 client 会直接抛异常,由上层统一捕获)
|
||||
if test_first:
|
||||
test_data = {
|
||||
"datasourceName": args.get("datasourceName"),
|
||||
@@ -100,10 +100,8 @@ class CreateDatasourceTool(ToolDef):
|
||||
"password": args.get("password"),
|
||||
"connectionType": args.get("connectionType", "user_password"),
|
||||
}
|
||||
test_result = await self.client.post("/datasource/connection/test", json_data=test_data)
|
||||
if test_result.get("code") != 200:
|
||||
return {"success": False, "error": f"连接测试失败: {test_result.get('msg', '未知错误')}"}
|
||||
|
||||
await self.client.post("/datasource/connection/test", json_data=test_data)
|
||||
|
||||
# 创建数据源
|
||||
return await self.client.post("/datasource/connection", json_data=args)
|
||||
|
||||
@@ -117,11 +115,13 @@ class UpdateDatasourceTool(ToolDef):
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "数据源 ID"},
|
||||
"datasourceName": {"type": "string", "description": "更新名称"},
|
||||
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
|
||||
"host": {"type": "string", "description": "更新地址"},
|
||||
"port": {"type": "integer", "description": "更新端口"},
|
||||
"databaseName": {"type": "string", "description": "更新数据库名"},
|
||||
"username": {"type": "string", "description": "更新用户名"},
|
||||
"password": {"type": "string", "description": "新密码(不传则不变)"},
|
||||
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型"},
|
||||
"remark": {"type": "string", "description": "更新描述"},
|
||||
},
|
||||
"required": ["id"],
|
||||
@@ -168,6 +168,82 @@ class DeleteDatasourceTool(ToolDef):
|
||||
except Exception as e:
|
||||
# 记录日志但继续删除
|
||||
logger.debug(f"停用数据源失败(可能已停用): {e}")
|
||||
|
||||
|
||||
# 删除数据源
|
||||
return await self.client.delete(f"/datasource/connection/{ds_id}")
|
||||
|
||||
|
||||
@register_tool("test_connection")
|
||||
class TestConnectionTool(ToolDef):
|
||||
name = "test_connection"
|
||||
description = "测试外部数据库连接是否可用(不创建数据源)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceType": {"type": "string", "enum": ["mysql", "postgresql", "oracle", "sqlserver", "dameng", "kingbase", "sqlite", "mariadb"], "description": "数据库类型"},
|
||||
"host": {"type": "string", "description": "数据库地址"},
|
||||
"port": {"type": "integer", "description": "端口号"},
|
||||
"databaseName": {"type": "string", "description": "要连接的数据库名"},
|
||||
"username": {"type": "string", "description": "数据库用户名"},
|
||||
"password": {"type": "string", "description": "密码"},
|
||||
"connectionType": {"type": "string", "enum": ["user_password", "ssl"], "description": "连接类型,默认 user_password"},
|
||||
},
|
||||
"required": ["datasourceType", "host", "port", "databaseName", "username"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
args.setdefault("connectionType", "user_password")
|
||||
return await self.client.post("/datasource/connection/test", json_data=args)
|
||||
|
||||
|
||||
@register_tool("get_realtime_structure")
|
||||
class GetRealtimeStructureTool(ToolDef):
|
||||
name = "get_realtime_structure"
|
||||
description = "拉取数据源真实的库表结构(实时探测远端数据库)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||
},
|
||||
"required": ["datasourceId"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
ds_id = args["datasourceId"]
|
||||
return await self.client.get(f"/datasource/connection/realtime/structure/{ds_id}")
|
||||
|
||||
|
||||
@register_tool("create_builtin_datasource")
|
||||
class CreateBuiltinDatasourceTool(ToolDef):
|
||||
name = "create_builtin_datasource"
|
||||
description = "创建内置 PostgreSQL 数据源连接,返回 connectionId(内置源建库建表链路第一步)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceName": {"type": "string", "description": "数据源名称(3-20字)"},
|
||||
"remark": {"type": "string", "description": "数据源描述"},
|
||||
},
|
||||
"required": ["datasourceName"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.post("/datasource/connection/create_builtin_postgresql", json_data=args)
|
||||
|
||||
|
||||
@register_tool("update_builtin_datasource")
|
||||
class UpdateBuiltinDatasourceTool(ToolDef):
|
||||
name = "update_builtin_datasource"
|
||||
description = "修改内置 PostgreSQL 数据源连接信息"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "数据源 ID"},
|
||||
"datasourceName": {"type": "string", "description": "更新名称"},
|
||||
"remark": {"type": "string", "description": "更新描述"},
|
||||
},
|
||||
"required": ["id"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.put("/datasource/connection/update_builtin_database", json_data=args)
|
||||
|
||||
151
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/mqtt_sync.py
Normal file
151
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/tools/mqtt_sync.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""
|
||||
MQTT 字段关联同步工具(内置源专属,文档 §8)
|
||||
|
||||
接口前缀为 /system/mqtt(注意与 /datasource 不同)。
|
||||
|
||||
关于 sourceTable / targetTable 的方向(已真机验证):
|
||||
直接调后端 API 时按字面透传即可——create 传什么,detail 回显就是什么,
|
||||
字段自洽、后端不做调换。例:create 传 sourceTable=sys_user/targetTable=users,
|
||||
detail 读回仍是 sourceTable=sys_user/targetTable=users。
|
||||
文档 §8 提到的「前端提交时调换」是前端 Vue 组件为 UI 展示所做的转换,
|
||||
与后端 API 契约无关,本工具(直连 API)无需调换。
|
||||
"""
|
||||
|
||||
from ._base import register_tool, ToolDef
|
||||
|
||||
|
||||
@register_tool("list_mqtt_configs")
|
||||
class ListMqttConfigsTool(ToolDef):
|
||||
name = "list_mqtt_configs"
|
||||
description = "获取 MQTT 字段关联同步配置列表"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||
"pageSize": {"type": "integer", "default": 20, "description": "每页数量"},
|
||||
},
|
||||
"required": [],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
params = {k: v for k, v in args.items() if v is not None}
|
||||
return await self.client.get("/system/mqtt/config/list", params=params)
|
||||
|
||||
|
||||
@register_tool("get_mqtt_config_detail")
|
||||
class GetMqttConfigDetailTool(ToolDef):
|
||||
name = "get_mqtt_config_detail"
|
||||
description = "获取 MQTT 同步配置详情"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"configId": {"type": "string", "description": "配置 ID"},
|
||||
},
|
||||
"required": ["configId"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.get(f"/system/mqtt/config/{args['configId']}")
|
||||
|
||||
|
||||
@register_tool("list_mqtt_target_tables")
|
||||
class ListMqttTargetTablesTool(ToolDef):
|
||||
name = "list_mqtt_target_tables"
|
||||
description = "获取 MQTT 可同步的目标表列表"
|
||||
input_schema = {"type": "object", "properties": {}, "required": []}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.get("/system/mqtt/config/tableList")
|
||||
|
||||
|
||||
@register_tool("list_mqtt_target_table_columns")
|
||||
class ListMqttTargetTableColumnsTool(ToolDef):
|
||||
name = "list_mqtt_target_table_columns"
|
||||
description = "获取 MQTT 目标表的字段列表"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableName": {"type": "string", "description": "目标表名"},
|
||||
},
|
||||
"required": ["tableName"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.get(f"/system/mqtt/config/tableColumns/{args['tableName']}")
|
||||
|
||||
|
||||
@register_tool("create_mqtt_config")
|
||||
class CreateMqttConfigTool(ToolDef):
|
||||
name = "create_mqtt_config"
|
||||
description = (
|
||||
"新增 MQTT 字段关联同步配置。"
|
||||
"sourceTable/targetTable 按字面透传即可(实测确认 create 存入与 detail 回显一致,"
|
||||
"无需调换;文档§8 的调换是前端 UI 层行为,与本 API 无关)"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"sourceTable": {"type": "string", "description": "源表名"},
|
||||
"targetTable": {"type": "string", "description": "目标表名"},
|
||||
"config": {"type": "object", "description": "完整配置体(字段映射等),按后端结构透传"},
|
||||
},
|
||||
"required": [],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
# config 若存在则以其为主体,否则直接用 args(去掉 config 包裹键)
|
||||
body = args.get("config") if isinstance(args.get("config"), dict) else dict(args)
|
||||
return await self.client.post("/system/mqtt/config", json_data=body)
|
||||
|
||||
|
||||
@register_tool("update_mqtt_config")
|
||||
class UpdateMqttConfigTool(ToolDef):
|
||||
name = "update_mqtt_config"
|
||||
description = (
|
||||
"修改 MQTT 字段关联同步配置。"
|
||||
"sourceTable/targetTable 按字面透传即可(实测确认无需调换,详见 create_mqtt_config)"
|
||||
)
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "配置 ID"},
|
||||
"sourceTable": {"type": "string", "description": "源表名"},
|
||||
"targetTable": {"type": "string", "description": "目标表名"},
|
||||
"config": {"type": "object", "description": "完整配置体,按后端结构透传"},
|
||||
},
|
||||
"required": ["id"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
if isinstance(args.get("config"), dict):
|
||||
body = dict(args["config"])
|
||||
body.setdefault("id", args.get("id"))
|
||||
else:
|
||||
body = dict(args)
|
||||
return await self.client.put("/system/mqtt/config", json_data=body)
|
||||
|
||||
|
||||
@register_tool("delete_mqtt_config")
|
||||
class DeleteMqttConfigTool(ToolDef):
|
||||
name = "delete_mqtt_config"
|
||||
description = "删除 MQTT 同步配置(支持多个 id,逗号拼接)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"ids": {"type": "string", "description": "配置 ID,多个用逗号拼接"},
|
||||
},
|
||||
"required": ["ids"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.delete(f"/system/mqtt/config/{args['ids']}")
|
||||
|
||||
|
||||
@register_tool("refresh_mqtt_config_cache")
|
||||
class RefreshMqttConfigCacheTool(ToolDef):
|
||||
name = "refresh_mqtt_config_cache"
|
||||
description = "刷新 MQTT 同步配置缓存"
|
||||
input_schema = {"type": "object", "properties": {}, "required": []}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
return await self.client.get("/system/mqtt/config/refreshCache")
|
||||
@@ -119,14 +119,17 @@ class DeleteSkillToolTool(ToolDef):
|
||||
@register_tool("update_skill_config")
|
||||
class UpdateSkillConfigTool(ToolDef):
|
||||
name = "update_skill_config"
|
||||
description = "更新技能配置(如 MCP Server 配置模板)"
|
||||
description = "更新技能(名称/描述/MCP 配置模板等)。对应后端 skill/updateOrGet"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||
"configTemplate": {"type": "string", "description": "配置模板 JSON 字符串"},
|
||||
"skillId": {"type": "string", "description": "技能 ID(可选)"},
|
||||
"name": {"type": "string", "description": "技能名称(可选)"},
|
||||
"description": {"type": "string", "description": "技能描述(可选)"},
|
||||
"configTemplate": {"type": "string", "description": "配置模板 JSON 字符串(可选)"},
|
||||
},
|
||||
"required": ["datasourceId", "configTemplate"],
|
||||
"required": ["datasourceId"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
@@ -135,3 +138,40 @@ class UpdateSkillConfigTool(ToolDef):
|
||||
if "configTemplate" in args and isinstance(args["configTemplate"], dict):
|
||||
args["configTemplate"] = json.dumps(args["configTemplate"])
|
||||
return await self.client.post("/datasource/skill/updateOrGet", json_data=args)
|
||||
|
||||
|
||||
@register_tool("update_skill_tool")
|
||||
class UpdateSkillToolTool(ToolDef):
|
||||
name = "update_skill_tool"
|
||||
description = "修改技能下某个工具的名称/描述/SQL等(对应后端 tskilltool/updateOrGet,upsert 语义)"
|
||||
# 真实工具实体字段(已真机探测确认):主键 id、展示名 uniqueName、描述 description、
|
||||
# SQL 模板 sqlTemplate、结果类型 resultType。后端不认 skillToolId/businessDescription/businessScenario。
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"id": {"type": "string", "description": "技能工具 ID(get_skill_tools 返回的 id)"},
|
||||
"uniqueName": {"type": "string", "description": "工具展示名(可选)"},
|
||||
"description": {"type": "string", "description": "工具描述(可选)"},
|
||||
"sqlTemplate": {"type": "string", "description": "SQL 模板(可选)"},
|
||||
"resultType": {"type": "string", "enum": ["single", "list"], "description": "结果类型(可选)"},
|
||||
},
|
||||
"required": ["id"],
|
||||
}
|
||||
|
||||
# 旧参数名 -> 真实字段名(向后兼容此前错误实现的调用方)
|
||||
_LEGACY_MAP = {
|
||||
"skillToolId": "id",
|
||||
"name": "uniqueName",
|
||||
"businessDescription": "description",
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
for old, new in self._LEGACY_MAP.items():
|
||||
if old in args and new not in args:
|
||||
args[new] = args.pop(old)
|
||||
else:
|
||||
args.pop(old, None)
|
||||
# businessScenario 后端实体无此字段,丢弃避免干扰
|
||||
args.pop("businessScenario", None)
|
||||
return await self.client.post("/datasource/skill/tskilltool/updateOrGet", json_data=args)
|
||||
|
||||
@@ -35,4 +35,11 @@ class ExecuteSqlTool(ToolDef):
|
||||
body["businessName"] = args["businessName"]
|
||||
if "params" in args:
|
||||
body["parameters"] = args["params"]
|
||||
return await self.client.post("/datasource/sqlExecutionLog/testSqlWithSchema", json_data=body)
|
||||
# target 通过 query 参数下发(prod/test 双环境),默认 prod
|
||||
target = args.get("target", "prod")
|
||||
params = {"target": target} if target else None
|
||||
return await self.client.post(
|
||||
"/datasource/sqlExecutionLog/testSqlWithSchema",
|
||||
json_data=body,
|
||||
params=params,
|
||||
)
|
||||
|
||||
@@ -8,22 +8,24 @@ from ._base import register_tool, ToolDef
|
||||
@register_tool("toggle_table_subscription")
|
||||
class ToggleTableSubscriptionTool(ToolDef):
|
||||
name = "toggle_table_subscription"
|
||||
description = "切换表的订阅状态"
|
||||
description = "切换表的订阅状态(订阅依赖该表已配置 MQTT 字段关联,否则后端会报操作失败)"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableId": {"type": "string", "description": "表 ID"},
|
||||
"datasourceId": {"type": "string", "description": "数据源 ID"},
|
||||
"configId": {"type": "string", "description": "库/配置 ID(即 list_databases 返回的 config id)"},
|
||||
"tableName": {"type": "string", "description": "表名(注意:后端按表名而非 tableId 识别)"},
|
||||
"subscribe": {"type": "boolean", "description": "true=订阅, false=取消订阅"},
|
||||
},
|
||||
"required": ["tableId", "datasourceId", "subscribe"],
|
||||
"required": ["configId", "tableName", "subscribe"],
|
||||
}
|
||||
|
||||
async def execute(self, args: dict) -> dict:
|
||||
# 映射参数名为后端 API 期望的格式
|
||||
# 后端真实字段(已真机探测确认):configId + tableName + isSubscribe(bool)
|
||||
# 兼容旧参数名 datasourceId->configId
|
||||
config_id = args.get("configId") or args.get("datasourceId")
|
||||
body = {
|
||||
"tableId": args["tableId"],
|
||||
"datasourceId": args["datasourceId"],
|
||||
"subscribe": args["subscribe"],
|
||||
"configId": config_id,
|
||||
"tableName": args.get("tableName"),
|
||||
"isSubscribe": args.get("subscribe"),
|
||||
}
|
||||
return await self.client.post("/datasource/subscription/toggle", json_data=body)
|
||||
|
||||
@@ -10,12 +10,12 @@ from ._base import register_tool, ToolDef
|
||||
@register_tool("query_table_data")
|
||||
class QueryTableDataTool(ToolDef):
|
||||
name = "query_table_data"
|
||||
description = "查询内置表数据(分页)"
|
||||
description = "查询内置表数据(分页)。target 默认 test(调试环境);查询线上数据须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableId": {"type": "string", "description": "表 ID"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test(调试);线上数据传 prod"},
|
||||
"pageNum": {"type": "integer", "default": 1, "description": "页码"},
|
||||
"pageSize": {"type": "integer", "default": 10, "description": "每页数量"},
|
||||
},
|
||||
@@ -25,6 +25,7 @@ class QueryTableDataTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
args.setdefault("target", "test")
|
||||
params = {k: v for k, v in args.items() if v is not None}
|
||||
return await self.client.get(f"/datasource/connection/builtin/table/{table_id}", params=params)
|
||||
|
||||
@@ -32,12 +33,12 @@ class QueryTableDataTool(ToolDef):
|
||||
@register_tool("insert_table_row")
|
||||
class InsertTableRowTool(ToolDef):
|
||||
name = "insert_table_row"
|
||||
description = "向内置表插入一行数据"
|
||||
description = "向内置表插入一行数据。⚠️ 写操作 target 默认 test(调试环境);写入线上库须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableId": {"type": "string", "description": "表 ID"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test(调试);写入线上库必须显式传 prod"},
|
||||
"data": {"type": "object", "description": "行数据(键值对,键为字段名)"},
|
||||
},
|
||||
"required": ["tableId", "data"],
|
||||
@@ -46,21 +47,22 @@ class InsertTableRowTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
target = args.pop("target", "prod")
|
||||
target = args.pop("target", "test")
|
||||
data = args.pop("data", {})
|
||||
params = {"target": target} if target else {}
|
||||
return await self.client.post(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=data, params=params)
|
||||
body = {"data": data}
|
||||
return await self.client.post(f"/datasource/connection/builtin/table/{table_id}/rows", json_data=body, params=params)
|
||||
|
||||
|
||||
@register_tool("update_table_row")
|
||||
class UpdateTableRowTool(ToolDef):
|
||||
name = "update_table_row"
|
||||
description = "更新内置表的指定行"
|
||||
description = "更新内置表的指定行。⚠️ 写操作 target 默认 test(调试环境);修改线上库须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableId": {"type": "string", "description": "表 ID"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test(调试);修改线上库必须显式传 prod"},
|
||||
"primaryKey": {"type": "object", "description": "主键值(如 {\"id\": 1})"},
|
||||
"data": {"type": "object", "description": "要更新的字段值"},
|
||||
},
|
||||
@@ -70,7 +72,7 @@ class UpdateTableRowTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
target = args.pop("target", "prod")
|
||||
target = args.pop("target", "test")
|
||||
primary_key = args.pop("primaryKey")
|
||||
data = args.pop("data", {})
|
||||
params = {"target": target} if target else {}
|
||||
@@ -81,12 +83,12 @@ class UpdateTableRowTool(ToolDef):
|
||||
@register_tool("delete_table_rows")
|
||||
class DeleteTableRowsTool(ToolDef):
|
||||
name = "delete_table_rows"
|
||||
description = "删除内置表的指定行(根据主键批量删除)"
|
||||
description = "删除内置表的指定行(根据主键批量删除)。⚠️ 写操作 target 默认 test(调试环境);删除线上库数据须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableId": {"type": "string", "description": "表 ID"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "⚠️ 环境,默认 test(调试);删除线上库数据必须显式传 prod"},
|
||||
"primaryKeys": {
|
||||
"type": "array",
|
||||
"description": "主键数组(如 [{\"id\": 1}, {\"id\": 2}])",
|
||||
@@ -99,7 +101,7 @@ class DeleteTableRowsTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
target = args.pop("target", "prod")
|
||||
target = args.pop("target", "test")
|
||||
primary_keys = args.pop("primaryKeys")
|
||||
params = {"target": target} if target else {}
|
||||
body = {"primaryKeys": primary_keys}
|
||||
@@ -109,12 +111,12 @@ class DeleteTableRowsTool(ToolDef):
|
||||
@register_tool("export_table_excel")
|
||||
class ExportTableExcelTool(ToolDef):
|
||||
name = "export_table_excel"
|
||||
description = "导出表数据为 Excel 文件(返回 base64 编码)"
|
||||
description = "导出表数据为 Excel 文件(返回 base64 编码)。target 默认 test(调试环境);导出线上数据须显式传 target=prod"
|
||||
input_schema = {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"tableId": {"type": "string", "description": "表 ID"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "prod", "description": "环境,默认 prod"},
|
||||
"target": {"type": "string", "enum": ["prod", "test"], "default": "test", "description": "环境,默认 test(调试);线上数据传 prod"},
|
||||
},
|
||||
"required": ["tableId"],
|
||||
}
|
||||
@@ -122,7 +124,7 @@ class ExportTableExcelTool(ToolDef):
|
||||
async def execute(self, args: dict) -> dict:
|
||||
args = dict(args)
|
||||
table_id = args.pop("tableId")
|
||||
target = args.pop("target", "prod")
|
||||
target = args.pop("target", "test")
|
||||
params = {"target": target} if target else {}
|
||||
result = await self.client.get(f"/datasource/connection/builtin/table/{table_id}/export/excel", params=params)
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -64,37 +64,49 @@ class AgileDBAPIClient:
|
||||
return headers
|
||||
|
||||
def _build_url(self, path: str) -> str:
|
||||
"""构建完整 URL,自动去掉路径中多余的 /api 前缀"""
|
||||
"""构建完整 URL
|
||||
|
||||
约定:base_url 已包含完整地址(如含 /api 前缀则配在环境变量里),
|
||||
各工具传入的 path 不带 /api 前缀,此处直接拼接。
|
||||
"""
|
||||
if path.startswith('http://') or path.startswith('https://'):
|
||||
return path
|
||||
# 去掉 /api 前缀,因为 base_url 已经包含完整地址
|
||||
clean_path = path
|
||||
if clean_path.startswith('/api'):
|
||||
clean_path = clean_path[4:]
|
||||
return f"{self.base_url}{clean_path}"
|
||||
return f"{self.base_url}{path}"
|
||||
|
||||
def _handle_response(self, response: httpx.Response, url: str) -> Dict[str, Any]:
|
||||
"""统一处理 API 响应"""
|
||||
logger.info(f"[API响应] HTTP {response.status_code}")
|
||||
|
||||
|
||||
if response.status_code == 204:
|
||||
return {"success": True, "data": None}
|
||||
|
||||
response.raise_for_status()
|
||||
|
||||
|
||||
# 先尝试解析 body,再判断状态码。
|
||||
# 平台会用非 2xx 状态码 + body 里的 {code, msg} 返回业务错误,
|
||||
# 若先 raise_for_status() 会在解析 body 前抛异常,导致真正的 msg 全部丢失。
|
||||
try:
|
||||
data = response.json()
|
||||
except json.JSONDecodeError:
|
||||
# 非 JSON 响应(如文件下载)
|
||||
except (json.JSONDecodeError, UnicodeDecodeError):
|
||||
# 非 JSON 响应(如 Excel/文件下载,二进制内容 .json() 会抛 UnicodeDecodeError)
|
||||
response.raise_for_status()
|
||||
return {"success": True, "data": response.content, "raw": True}
|
||||
|
||||
|
||||
# 检查平台 API 的 {code, msg} 格式
|
||||
# 平台约定:code 为 200 或 0 均表示成功(见数据库模块文档 §1.1)
|
||||
if isinstance(data, dict) and 'code' in data:
|
||||
if data['code'] != 200:
|
||||
if data['code'] not in (0, 200):
|
||||
error_msg = data.get('msg', '未知错误')
|
||||
logger.error(f"[API错误] {error_msg}")
|
||||
logger.error(f"[API错误] HTTP {response.status_code}, {error_msg}")
|
||||
raise Exception(error_msg)
|
||||
|
||||
# code 合法即视为业务成功,不再用 HTTP 状态码二次否决
|
||||
return data
|
||||
|
||||
# 无 {code} 信封的响应:回落到 HTTP 状态码判断,
|
||||
# 非 2xx 时把 body 文本带进异常,避免错误细节丢失。
|
||||
if response.status_code >= 400:
|
||||
detail = data if isinstance(data, str) else json.dumps(data, ensure_ascii=False)
|
||||
logger.error(f"[API错误] HTTP {response.status_code}, {detail}")
|
||||
raise Exception(f"HTTP {response.status_code}: {detail}")
|
||||
|
||||
return data
|
||||
|
||||
async def get(self, path: str, params: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
@@ -147,9 +159,13 @@ class AgileDBAPIClient:
|
||||
try:
|
||||
logger.info(f"[API请求] DELETE {url}")
|
||||
headers = self._get_headers()
|
||||
# 注意:httpx 的 client.delete() 便捷方法不接受 json 参数(仅 post/put/patch 有)。
|
||||
# 需要带 body 的 DELETE 必须走通用 request(),否则会抛 TypeError。
|
||||
if json_data is not None:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
response = await self.client.delete(url, headers=headers, params=params, json=json_data)
|
||||
response = await self.client.request("DELETE", url, headers=headers, params=params, json=json_data)
|
||||
else:
|
||||
response = await self.client.delete(url, headers=headers, params=params)
|
||||
return self._handle_response(response, url)
|
||||
except httpx.TimeoutException:
|
||||
raise Exception(f"API 请求超时: {url}")
|
||||
|
||||
@@ -44,7 +44,7 @@ def get_env_config() -> dict:
|
||||
dict: 包含所有配置的字典
|
||||
"""
|
||||
return {
|
||||
"api_key": get_api_key(""),
|
||||
"api_key": os.environ.get("API_KEY", ""),
|
||||
"base_url": get_base_url(),
|
||||
}
|
||||
|
||||
|
||||
454
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库模块-功能与业务链路说明.md
Normal file
454
lzwcai_mcp_agile_db/lzwcai_mcp_agile_db/数据库模块-功能与业务链路说明.md
Normal file
@@ -0,0 +1,454 @@
|
||||
# 数据库模块 —— 功能、接口与业务链路说明
|
||||
|
||||
> 适用范围:`src/components/databasePage/` 全部组件 + `src/server/database.ts` API 层
|
||||
> 数据源分两类:**外置数据源(external)** 与 **内置数据源(builtin / 内置 PostgreSQL)**
|
||||
> 所有接口前缀均为 `/api/datasource`(MQTT 同步相关为 `/api/system/mqtt`)
|
||||
> 大部分接口超时设置为 `300000ms`(5 分钟,因含 AI 处理)
|
||||
|
||||
---
|
||||
|
||||
## 0. 名词与基础概念
|
||||
|
||||
| 名词 | 说明 |
|
||||
| --- | --- |
|
||||
| 连接 / 数据源(connection) | 一条数据库连接记录,`sourceType` 为 `external` 或 `builtin` |
|
||||
| 配置(config) | 数据源下「数据库 + 选中表」的关联配置,一条 config 对应一个库 |
|
||||
| 表元数据(table metadata) | 平台侧记录的表结构(字段、注释、AI 训练描述等) |
|
||||
| 技能(skill) | 绑定到某数据源的「智能问数技能」 |
|
||||
| 工具(tool) | 技能下的一条可复用 SQL 查询能力(沉淀为 MCP 工具) |
|
||||
| 环境(target) | 内置数据源数据分 `prod`(线上)/ `test`(调试)两套 |
|
||||
| status | 数据源状态:`0` 运行中,`1` 已停止 |
|
||||
|
||||
### 两类数据源核心差异
|
||||
|
||||
| 维度 | 外置数据源 external | 内置数据源 builtin |
|
||||
| --- | --- | --- |
|
||||
| 来源 | 连接用户已有的远程数据库 | 平台内置 PostgreSQL,平台直接建库建表 |
|
||||
| 默认数字员工 | `1001`(SQL处理助手) | `1002`(SQL处理助手) |
|
||||
| 建表能力 | ❌ 只读连接,不能建表 | ✅ AI 智能建表 / Excel 导入建表 / 手动建表 |
|
||||
| 数据增删改 | ❌ 不提供行级 CRUD | ✅ 行级增删改查 + Excel 导入导出 |
|
||||
| 环境切换 | ❌ 无 | ✅ prod / test 双环境 |
|
||||
| 表订阅同步 | ❌ | ✅ MQTT 数据变更订阅 |
|
||||
| 创建入口 | `CreateDataSource.vue` | `CreateBuiltinDataSource.vue` |
|
||||
|
||||
---
|
||||
|
||||
## 1. 数据源(连接)的增删改查 —— 内外置通用
|
||||
|
||||
入口:[DataSourcePageMain.vue](../src/components/databasePage/DataSourcePageMain.vue) → [DataSourceList.vue](../src/components/databasePage/DataSourceList.vue)
|
||||
|
||||
### 1.1 查询(列表 / 详情)
|
||||
|
||||
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 数据源列表(分页/搜索/状态筛选) | `fetchDataSources()` | `getConnectionList(params)` | GET | `/api/datasource/connection/list` |
|
||||
| 数据源详情 | `fetchDataSourceDetail()` | `getConnectionDetail(id)` | GET | `/api/datasource/connection/{id}` |
|
||||
| 连接实例详情(含库表结构) | — | `getConnectionInstanceDetail(id)` | GET | `/api/datasource/connection/{id}` |
|
||||
| 查看数据源实时库表结构 | `loadDatabaseList()` | `getConnectionRealtimeStructure(id)` | GET | `/api/datasource/connection/realtime/structure/{id}` |
|
||||
|
||||
- 列表查询参数:`{ datasourceName?, status?, pageNum, pageSize }`,前端用手动 `IntersectionObserver` 做无限滚动。
|
||||
- 响应判定统一:`code === 200 || code === 0` 视为成功,列表数据在 `response.rows`,总数在 `response.total`。
|
||||
|
||||
### 1.2 启用 / 停用
|
||||
|
||||
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 切换运行状态 | `handleChangeStatus()` | `putConnectionChangeStatus({ id, status })` | PUT | `/api/datasource/connection/changeStatus` |
|
||||
|
||||
- `status` 切换:`1->0` 启用,`0->1` 停用。
|
||||
|
||||
### 1.3 删除
|
||||
|
||||
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 删除数据源 | `handleDelete()` | `deleteConnection(id)` | DELETE | `/api/datasource/connection/{id}` |
|
||||
|
||||
**业务链路(删除)**:
|
||||
```
|
||||
用户点删除 → Modal 二次确认
|
||||
→ 若 status===0(运行中):先 putConnectionChangeStatus({id, status:1}) 停用
|
||||
→ 等待 500ms 确保状态更新
|
||||
→ deleteConnection(id)
|
||||
→ 成功后 fetchDataSources() 刷新列表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 外置数据源(external)的创建与编辑
|
||||
|
||||
入口组件:[CreateDataSource.vue](../src/components/databasePage/CreateDataSource.vue)(4 步向导)
|
||||
|
||||
### 2.1 步骤与接口
|
||||
|
||||
```
|
||||
步骤0 选择数据库类型(mysql/postgresql/oracle/sqlserver/dameng/kingbase/sqlite/mariadb)
|
||||
↓
|
||||
步骤1 配置连接信息(host/port/库名/用户名/密码/认证方式)
|
||||
→ 点「下一步」自动触发 testConnection() 测试连接
|
||||
→ 测试通过 → postConnectionDetail() / putConnectionDetail() 创建/更新数据源
|
||||
→ emit('refresh') 通知列表刷新
|
||||
→ getConnectionRealtimeStructure() 拉取真实库表结构
|
||||
↓
|
||||
步骤2 选择数据库和数据表(大数据量:shallowRef + 分批渲染 + 防抖搜索)
|
||||
↓
|
||||
步骤3 确认信息
|
||||
→ postConnectionConfig() / putConnectionConfig() 落库「库表关联」配置
|
||||
→ emit('success')
|
||||
```
|
||||
|
||||
### 2.2 接口清单
|
||||
|
||||
| 功能 | 接口函数 | 方法 | 路径 | 关键入参 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 测试连接 | `testConnection(data)` | POST | `/api/datasource/connection/test` | host/port/databaseName/datasourceType/username/password/connectionType |
|
||||
| 创建数据源 | `postConnectionDetail(data)` | POST | `/api/datasource/connection` | 同上 + datasourceName/remark |
|
||||
| 更新数据源 | `putConnectionDetail(data)` | PUT | `/api/datasource/connection` | 同上 + id(编辑时密码可空表示不改) |
|
||||
| 拉取实时库表结构 | `getConnectionRealtimeStructure(id)` | GET | `/api/datasource/connection/realtime/structure/{id}` | 数据源 id |
|
||||
| 创建库表关联配置 | `postConnectionConfig(data)` | POST | `/api/datasource/config` | connectionId / syncTables / databases[] |
|
||||
| 更新库表关联配置 | `putConnectionConfig(data)` | PUT | `/api/datasource/config` | id + 同上 |
|
||||
| 删除配置 | `deleteConnectionConfig(ids[])` | POST | `/api/datasource/config/deletes` | `{ ids: [] }` |
|
||||
|
||||
- `postConnectionConfig` 请求体结构:
|
||||
```json
|
||||
{
|
||||
"id": "配置ID(编辑传)",
|
||||
"connectionId": "数据源ID",
|
||||
"syncTables": true,
|
||||
"databases": [
|
||||
{ "databaseName": "库名", "tableNames": ["表1", "表2"] }
|
||||
]
|
||||
}
|
||||
```
|
||||
- 编辑模式不回显密码;密码留空 = 不修改密码。
|
||||
|
||||
---
|
||||
|
||||
## 3. 内置数据源(builtin / 内置 PostgreSQL)的创建与编辑
|
||||
|
||||
入口组件:[CreateBuiltinDataSource.vue](../src/components/databasePage/CreateBuiltinDataSource.vue)
|
||||
支持三种模式:`create`(新建数据源)/ `edit`(编辑数据源)/ `addTable`(为已有库新增表)。
|
||||
|
||||
### 3.1 步骤与接口(create 模式)
|
||||
|
||||
```
|
||||
步骤0 填写数据源信息(datasourceName / remark)
|
||||
→ postCreateBuiltinPostgreSQLConnection() 创建内置 PG 连接,返回 connectionId
|
||||
↓
|
||||
步骤1 填写业务场景描述 + 数据库名(仅字母数字下划线,自动加 _数据源id 后缀)
|
||||
→ AI 生成表结构:postGenerateTable() 返回 taskId
|
||||
→ 轮询 getAiTrainingDetail(taskId)(trainingStatus: 0待训/1训练中/2完成/3失败,最多 60 次 × 2s)
|
||||
→ 解析 createTableData.data.tables 填入表列表
|
||||
→ postCreateDatabase(connectionId, { databaseName }) 创建数据库
|
||||
↓
|
||||
步骤2 预览 / 编辑表结构(TableDetailEditor)
|
||||
→ handleSubmit() 遍历表逐张 postCreateTable(connectionId, requestData)
|
||||
→ emit('refresh') + emit('success')
|
||||
```
|
||||
|
||||
### 3.2 接口清单
|
||||
|
||||
| 功能 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| 创建内置 PG 连接 | `postCreateBuiltinPostgreSQLConnection(data)` | POST | `/api/datasource/connection/create_builtin_postgresql` |
|
||||
| 修改内置 PG 连接 | `putUpdateBuiltinDatabase(data)` | PUT | `/api/datasource/connection/update_builtin_database` |
|
||||
| AI 生成表结构 | `postGenerateTable(data)` | POST | `/api/datasource/connection/generate_table` |
|
||||
| 查询 AI 训练任务详情(轮询) | `getAiTrainingDetail(taskId)` | GET | `/api/ai/training/{taskId}` |
|
||||
| 创建数据库 | `postCreateDatabase(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_database` |
|
||||
| 创建数据表 | `postCreateTable(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_table` |
|
||||
| 创建库+表(合并) | `postCreateDatabaseTable(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_database_table` |
|
||||
|
||||
- `addTable` 模式不直接调建表接口,而是 `emit('submitTables', tables)` 交回父组件(如 DatabaseDetail)提交。
|
||||
- AI 业务描述润色走 `getAgentGeneric` + `AGENT_GENERIC_CODES.SQL_SCENARIO_DESCRIPTION`(`server/employee`)。
|
||||
|
||||
---
|
||||
|
||||
## 4. 数据库 / 表结构的增删改查(主要面向内置源)
|
||||
|
||||
入口组件:[DatabaseDetail.vue](../src/components/databasePage/DatabaseDetail.vue)(左侧表列表 + 右侧四视图)
|
||||
|
||||
### 4.1 表 / 字段的查询
|
||||
|
||||
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 表元数据列表(分页/无限滚动) | `getTablesList()` | `getTableAiList(params)` | GET | `/api/datasource/table/ailist` |
|
||||
| 表详情(字段列表) | `executeSelectTable()` | `getTableDetail(id)` | GET | `/api/datasource/table/{id}/detail` |
|
||||
| 表列表(基础) | — | `getTableList(data)` | GET | `/api/datasource/table/list` |
|
||||
|
||||
- `getTableAiList` 参数:`{ datasourceId, pageNum, pageSize }`(这里 `datasourceId` 实为「配置/库」id)。
|
||||
- 切表有 Race Condition 防护:对比 `currentTableId` 拦截过期的详情请求。
|
||||
|
||||
### 4.2 库 / 表的增删改
|
||||
|
||||
| 功能 | 组件方法 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 修改数据库(改名) | `handleDatabaseSettingsConfirm()` | `putAlterDatabase(connectionId, data)` | PUT | `/api/datasource/connection/{connectionId}/alter_database` |
|
||||
| 删除库(配置) | `handleDeleteDatabase()` | `deleteConnectionConfig(ids[])` | POST | `/api/datasource/config/deletes` |
|
||||
| AI 智能建表(入口) | `createNewTable()` | 仅打开弹窗(内嵌 `CreateBuiltinDataSource` 的 addTable 模式) | — | — |
|
||||
| 批量建表(提交) | `handleSubmitTables(tables)` | 循环 `postCreateTable(connectionId, data)` | POST | `/api/datasource/connection/{connectionId}/create_table` |
|
||||
| 修改表结构 | `handleSaveTable()` | `putAlterTable(connectionId, data)` | PUT | `/api/datasource/connection/{connectionId}/alter_table` |
|
||||
|
||||
- 修改库后会重新 `getConnectionDetail(id)` 并 `emit('refresh', updatedDataSource)` 同步父级。
|
||||
- 改表用增量 operation 标记字段:`ADD_COLUMN` / `MODIFY_COLUMN` / `DROP_COLUMN`(来自 TableDetailEditor)。
|
||||
|
||||
### 4.3 智能导入建表(Excel)
|
||||
|
||||
入口组件:[TableRecognition.vue](../src/components/databasePage/TableRecognition.vue)(两步向导)
|
||||
|
||||
```
|
||||
步骤0 上传 Excel(≤500KB)+ 选环境
|
||||
→ postImportDocumentPreview(connectionId, file, target) AI 识别前10条 → 表结构预览
|
||||
↓
|
||||
步骤1 编辑确认表结构与数据
|
||||
→ postImportDocumentConfirm(connectionId, data, {target}) 建表 + 插入数据
|
||||
→ emit('complete')
|
||||
```
|
||||
|
||||
| 功能 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| 文档导入预览(AI识别) | `postImportDocumentPreview(connectionId, file, target)` | POST | `/api/datasource/connection/{connectionId}/import_document/preview` |
|
||||
| 文档导入确认(建表+插数据) | `postImportDocumentConfirm(connectionId, data, params)` | POST | `/api/datasource/connection/{connectionId}/import_document/confirm` |
|
||||
|
||||
---
|
||||
|
||||
## 5. 内置表「数据行」的增删改查(builtin 专属,prod/test 双环境)
|
||||
|
||||
入口组件:[CustomizeDbTable.vue](../src/components/databasePage/CustomizeDbTable.vue)(包裹通用 `CommonDbTable`)
|
||||
所有接口都带 `target` 参数区分线上 / 调试环境。
|
||||
|
||||
| 功能 | 组件回调 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 查(分页拉数据) | `onFetch` | `getBuiltinTableData(tableId, {pageNum,pageSize,target})` | GET | `/api/datasource/connection/builtin/table/{tableId}` |
|
||||
| 增(新增行) | `onAdd` | `postBuiltinTableRows(tableId, data, {target})` | POST | `/api/datasource/connection/builtin/table/{tableId}/rows` |
|
||||
| 改(更新行) | `onEdit` | `putBuiltinTableRows(tableId, data, {target})` | PUT | `/api/datasource/connection/builtin/table/{tableId}/rows` |
|
||||
| 删(删除行) | `onDelete` | `deleteBuiltinTableRows(tableId, {primaryKeys}, {target})` | DELETE | `/api/datasource/connection/builtin/table/{tableId}/rows` |
|
||||
| 导出 Excel | `onExport` | `getBuiltinTableExportExcel(tableId, {target})` | GET | `/api/datasource/connection/builtin/table/{tableId}/export/excel` |
|
||||
| 导入数据 | `onImport` → `handleImportComplete` | `postImportDocumentConfirm(...)` | POST | 见上 |
|
||||
|
||||
- `onFetch` 把后端二维数组 `content` 按列名映射成对象数组。
|
||||
- `onEdit/onDelete` 用动态主键(`rowKey`,默认 `id`)拼装 `primaryKey` / `primaryKeys`。
|
||||
- 删除入参结构:`{ primaryKeys: [ { 主键字段: 值 }, ... ] }`。
|
||||
- 导出接口为 `responseType: 'blob'`,含完整错误兜底(blob 是 JSON 时解析出 msg)。
|
||||
|
||||
---
|
||||
|
||||
## 6. 智能问数:技能(skill)与工具(tool)的增删改查
|
||||
|
||||
入口组件:[ChatDebugging.vue](../src/components/databasePage/ChatDebugging.vue) + [SqlControllerMsg.vue](../src/components/databasePage/SqlControllerMsg.vue)
|
||||
|
||||
### 6.1 接口清单
|
||||
|
||||
| 功能 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| 查询数据源配置(含 skillBool) | `getConnectionConfig(id)` | GET | `/api/datasource/config/{id}` |
|
||||
| 按数据源查技能 | `getSkillByDatasource(id)` | GET | `/api/datasource/skill/getByDatasource/{id}` |
|
||||
| 按技能 ID 查工具列表 | `getSkillBySkillId(id)` | GET | `/api/datasource/skill/getBySkillId/{id}` |
|
||||
| 创建技能 | `postSkillCreateOrGet(data)` | POST | `/api/datasource/skill/createOrGet` |
|
||||
| 修改技能 | `putSkillUpdateOrGet(data)` | POST | `/api/datasource/skill/updateOrGet` |
|
||||
| 确认/创建技能工具 | `postSqlSkillConfirmTools(data)` | POST | `/api/datasource/skill/confirmTools` |
|
||||
| 修改工具名/描述 | `postSkillToolUpdateOrGet(data)` | POST | `/api/datasource/skill/tskilltool/updateOrGet` |
|
||||
| 删除技能工具 | `postDeleteSkillTool(skillToolId)` | DELETE | `/api/datasource/skill/tskilltool/{skillToolId}` |
|
||||
|
||||
> 说明:`executeSql`(POST `/api/datasource/sqlExecutionLog/testSqlWithSchema`)虽在 `server/database.ts` 中定义,但在 databasePage 模块内**未被任何组件调用**——它仅在工作流编辑器 [workflowPage/EditorMain.vue](../src/components/workflowPage/EditorMain.vue) 里使用。智能问数场景下,SQL 由后端 AI 通过 SSE 流执行并直接返回结果,前端不再单独调用执行接口。
|
||||
|
||||
### 6.2 「把查询沉淀为工具」业务链路(SqlControllerMsg 核心)
|
||||
|
||||
```
|
||||
AI 返回 msgType=5 的 SQL 结果 → SqlControllerMsg 解析多视图
|
||||
用户点「添加到工具」
|
||||
→ 校验环境(test 禁止)、是否选库、重复检测
|
||||
→ 分两条路径:
|
||||
┌ skillBool=true(技能已存在)
|
||||
│ → postSqlSkillConfirmTools() 直接确认工具
|
||||
└ skillBool=false(技能不存在)
|
||||
→ postSkillCreateOrGet() 创建技能
|
||||
→ putSkillUpdateOrGet() 写入 MCP 配置模板 lzwcai-mcp-sqlexecutor
|
||||
→ postSqlSkillConfirmTools() 确认工具
|
||||
→ eventBus.emit(RELOAD_SKILL_DATA, datasourceId)
|
||||
→ ChatDebugging 监听到事件 → loadSkillData() 刷新左侧技能/工具列表
|
||||
```
|
||||
|
||||
- 重复检测链路:`getConnectionConfig → getSkillByDatasource → getSkillBySkillId`,比对 `sqlTemplate`。
|
||||
|
||||
---
|
||||
|
||||
## 7. AI 补全训练
|
||||
|
||||
入口:[DatabaseDetail.vue](../src/components/databasePage/DatabaseDetail.vue)「AI补全管理」视图
|
||||
|
||||
| 功能 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| 训练任务列表 | `getAiTrainingList(data)` | GET | `/api/ai/training/list` |
|
||||
| 按选中表创建训练 | `postAiTrainingCreateBySelected(data)` | POST | `/api/ai/training/createBySelected` |
|
||||
| 训练任务详情 | `getAiTrainingDetail(id)` | GET | `/api/ai/training/{id}` |
|
||||
|
||||
- 训练状态:`0` 待训练 / `1` 训练中 / `2` 已完成 / `3` 失败。
|
||||
|
||||
---
|
||||
|
||||
## 8. 字段关联同步(MQTT,builtin 专属)
|
||||
|
||||
入口:[DatabaseDetail.vue](../src/components/databasePage/DatabaseDetail.vue)「字段关联管理」视图
|
||||
|
||||
| 功能 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| 同步配置列表 | `getMqttConfigList(params)` | GET | `/api/system/mqtt/config/list` |
|
||||
| 配置详情 | `getMqttConfigDetail(configId)` | GET | `/api/system/mqtt/config/{configId}` |
|
||||
| 目标表列表 | `getMqttConfigTableList()` | GET | `/api/system/mqtt/config/tableList` |
|
||||
| 目标表字段列表 | `getMqttConfigTableColumns(tableName)` | GET | `/api/system/mqtt/config/tableColumns/{tableName}` |
|
||||
| 新增配置 | `postMqttConfig(data)` | POST | `/api/system/mqtt/config` |
|
||||
| 修改配置 | `putMqttConfig(data)` | PUT | `/api/system/mqtt/config` |
|
||||
| 删除配置 | `deleteMqttConfig(ids)` | DELETE | `/api/system/mqtt/config/{ids}` |
|
||||
| 刷新缓存 | `getMqttConfigRefreshCache()` | GET | `/api/system/mqtt/config/refreshCache` |
|
||||
| 切换表订阅同步 | `postDatasourceSubscriptionToggle(data)` | POST | `/api/datasource/subscription/toggle` |
|
||||
|
||||
> ⚠️ 注意:提交配置时前端故意调换 `sourceTable` / `targetTable`,回显时再换回来(见 DatabaseDetail 的 `applyRelationConfigByTargetTable` 与 `loadRelationConfigByTableId`)。
|
||||
|
||||
---
|
||||
|
||||
## 9. API 密钥与权限的增删改查
|
||||
|
||||
入口组件:[DataSourceKeys.vue](../src/components/databasePage/DataSourceKeys.vue) + [DataSourceKeySetting.vue](../src/components/databasePage/DataSourceKeySetting.vue)
|
||||
|
||||
### 9.1 密钥 CRUD
|
||||
|
||||
| 功能 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| 密钥列表 | `getApiKeyList(data)` | GET | `/api/datasource/api_key/list` |
|
||||
| 新增密钥 | `postApiKey(data)` | POST | `/api/datasource/api_key` |
|
||||
| 修改密钥(含启停) | `putApiKey(data)` | PUT | `/api/datasource/api_key` |
|
||||
| 删除密钥 | `deleteApiKey(ids)` | DELETE | `/api/datasource/api_key/{ids}` |
|
||||
|
||||
### 9.2 权限配置
|
||||
|
||||
| 功能 | 接口函数 | 方法 | 路径 |
|
||||
| --- | --- | --- | --- |
|
||||
| 查询密钥权限 | `getApiKeyPermission(apiKeyId)` | GET | `/api/datasource/api_key/permission/{apiKeyId}` |
|
||||
| 批量授予权限 | `postApiKeyPermissionGrantBatch(data)` | POST | `/api/datasource/api_key/permission/grant_batch` |
|
||||
|
||||
权限配置走 `DataSourceKeySetting` 四步向导(数据源 → 数据库 → 数据表),分三级权限(connection / database / table)。其中加载列表复用:
|
||||
|
||||
| 步骤 | 接口函数 | 路径 |
|
||||
| --- | --- | --- |
|
||||
| 选数据源 | `getConnectionList` | `/api/datasource/connection/list` |
|
||||
| 选数据库 | `getConnectionConfigList` | `/api/datasource/config/list` |
|
||||
| 选数据表 | `getTableList` | `/api/datasource/table/list` |
|
||||
|
||||
`postApiKeyPermissionGrantBatch` 请求体(batchDatas)每项:
|
||||
```json
|
||||
{
|
||||
"apiKeyId": "密钥ID",
|
||||
"batchDatas": [
|
||||
{
|
||||
"connectionId": "数据源ID",
|
||||
"permissionLevel": "connection|database|table",
|
||||
"permissionType": "read,write(逗号拼接)",
|
||||
"databaseName": "库名(database/table 级)",
|
||||
"tableName": "表名(table 级)"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. 端到端业务链路总览
|
||||
|
||||
### 链路 A:外置数据源「从创建到智能问数」
|
||||
```
|
||||
DataSourcePageMain → 添加远程数据源
|
||||
→ CreateDataSource 4步:
|
||||
testConnection → postConnectionDetail → getConnectionRealtimeStructure → postConnectionConfig
|
||||
→ 列表出现卡片 → 点「业务执行」
|
||||
→ ChatDebugging(员工1001)三栏 → DataSourcePlugIn 选库 → ChatBusiness 提问
|
||||
→ 后端 AI 经 SSE 流执行 SQL 并返回结果 (msgType=5) → SqlControllerMsg 多视图渲染
|
||||
→ 可「添加到工具」沉淀为技能
|
||||
```
|
||||
|
||||
### 链路 B:内置数据源「从建库建表到数据管理」
|
||||
```
|
||||
DataSourcePageMain → 添加内置数据源
|
||||
→ CreateBuiltinDataSource:
|
||||
postCreateBuiltinPostgreSQLConnection
|
||||
→ postGenerateTable + 轮询 getAiTrainingDetail(AI 建表结构)
|
||||
→ postCreateDatabase → postCreateTable
|
||||
→ DatabaseDetail 管理:
|
||||
getTableAiList(表列表)→ getTableDetail(字段)
|
||||
→ CustomizeDbTable 行级 CRUD(prod/test)
|
||||
→ 智能导入 TableRecognition(preview→confirm)
|
||||
→ 字段关联 MQTT / AI 补全训练 / 订阅
|
||||
→ 业务执行(员工1002)智能问数同链路 A
|
||||
```
|
||||
|
||||
### 链路 C:对外开放(API 密钥)
|
||||
```
|
||||
DataSourceKeys → postApiKey 建密钥
|
||||
→ DataSourceKeySetting 配权限(连接→库→表三级)
|
||||
→ postApiKeyPermissionGrantBatch 批量授权
|
||||
→ ApiCallDocument 查看调用文档(/API_DOCUMENTATION.md)
|
||||
→ 外部系统凭密钥调用数据查询
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录:完整接口索引(按 server/database.ts 顺序)
|
||||
|
||||
| # | 函数 | 方法 | 路径 | 用途 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| 1 | `getTableDetail` | GET | `/api/datasource/table/{id}/detail` | 表详情 |
|
||||
| 2 | `getConnectionList` | GET | `/api/datasource/connection/list` | 连接列表 |
|
||||
| 3 | `getConnectionDetail` | GET | `/api/datasource/connection/{id}` | 连接详情 |
|
||||
| 4 | `getConnectionInstanceDetail` | GET | `/api/datasource/connection/{id}` | 连接实例详情 |
|
||||
| 5 | `getTableList` | GET | `/api/datasource/table/list` | 表列表 |
|
||||
| 6 | `getMqttConfigList` | GET | `/api/system/mqtt/config/list` | MQTT配置列表 |
|
||||
| 7 | `getMqttConfigDetail` | GET | `/api/system/mqtt/config/{configId}` | MQTT配置详情 |
|
||||
| 8 | `getMqttConfigTableList` | GET | `/api/system/mqtt/config/tableList` | MQTT目标表 |
|
||||
| 9 | `getMqttConfigTableColumns` | GET | `/api/system/mqtt/config/tableColumns/{tableName}` | MQTT表字段 |
|
||||
| 10 | `postMqttConfig` | POST | `/api/system/mqtt/config` | 新增MQTT配置 |
|
||||
| 11 | `putMqttConfig` | PUT | `/api/system/mqtt/config` | 改MQTT配置 |
|
||||
| 12 | `deleteMqttConfig` | DELETE | `/api/system/mqtt/config/{ids}` | 删MQTT配置 |
|
||||
| 13 | `getMqttConfigRefreshCache` | GET | `/api/system/mqtt/config/refreshCache` | 刷新缓存 |
|
||||
| 14 | `getTableAiList` | GET | `/api/datasource/table/ailist` | 表元数据列表 |
|
||||
| 15 | `testConnection` | POST | `/api/datasource/connection/test` | 测试连接 |
|
||||
| 16 | `postConnectionDetail` | POST | `/api/datasource/connection` | 创建连接 |
|
||||
| 17 | `postCreateBuiltinPostgreSQLConnection` | POST | `/api/datasource/connection/create_builtin_postgresql` | 创建内置PG |
|
||||
| 18 | `postCreateDatabaseTable` | POST | `/api/datasource/connection/{id}/create_database_table` | 创建库+表 |
|
||||
| 19 | `postCreateTable` | POST | `/api/datasource/connection/{id}/create_table` | 创建表 |
|
||||
| 20 | `postCreateDatabase` | POST | `/api/datasource/connection/{id}/create_database` | 创建库 |
|
||||
| 21 | `putAlterDatabase` | PUT | `/api/datasource/connection/{id}/alter_database` | 改库 |
|
||||
| 22 | `putAlterTable` | PUT | `/api/datasource/connection/{id}/alter_table` | 改表 |
|
||||
| 23 | `postGenerateTable` | POST | `/api/datasource/connection/generate_table` | AI生成表结构 |
|
||||
| 24 | `putUpdateBuiltinDatabase` | PUT | `/api/datasource/connection/update_builtin_database` | 改内置PG |
|
||||
| 25 | `putConnectionDetail` | PUT | `/api/datasource/connection` | 更新连接 |
|
||||
| 26 | `deleteConnection` | DELETE | `/api/datasource/connection/{id}` | 删连接 |
|
||||
| 27 | `deleteConnectionConfig` | POST | `/api/datasource/config/deletes` | 删配置 |
|
||||
| 28 | `putConnectionConfig` | PUT | `/api/datasource/config` | 改库表配置 |
|
||||
| 29 | `postConnectionConfig` | POST | `/api/datasource/config` | 建库表配置 |
|
||||
| 30 | `getConnectionRealtimeStructure` | GET | `/api/datasource/connection/realtime/structure/{id}` | 实时库表结构 |
|
||||
| 31 | `getSkillByDatasource` | GET | `/api/datasource/skill/getByDatasource/{id}` | 按源查技能 |
|
||||
| 32 | `getSkillBySkillId` | GET | `/api/datasource/skill/getBySkillId/{id}` | 查工具列表 |
|
||||
| 33 | `postSkillCreateOrGet` | POST | `/api/datasource/skill/createOrGet` | 建技能 |
|
||||
| 34 | `putSkillUpdateOrGet` | POST | `/api/datasource/skill/updateOrGet` | 改技能 |
|
||||
| 35 | `postDeleteSkillTool` | DELETE | `/api/datasource/skill/tskilltool/{id}` | 删工具 |
|
||||
| 36 | `postSkillToolUpdateOrGet` | POST | `/api/datasource/skill/tskilltool/updateOrGet` | 改工具 |
|
||||
| 37 | `getAiTrainingList` | GET | `/api/ai/training/list` | 训练列表 |
|
||||
| 38 | `postAiTrainingCreateBySelected` | POST | `/api/ai/training/createBySelected` | 建训练 |
|
||||
| 39 | `getAiTrainingDetail` | GET | `/api/ai/training/{id}` | 训练详情 |
|
||||
| 40 | `putConnectionChangeStatus` | PUT | `/api/datasource/connection/changeStatus` | 启停连接 |
|
||||
| 41 | `getConnectionConfig` | GET | `/api/datasource/config/{id}` | 查源配置 |
|
||||
| 42 | `getConnectionConfigList` | GET | `/api/datasource/config/list` | 配置列表 |
|
||||
| 43 | `postSqlSkillConfirmTools` | POST | `/api/datasource/skill/confirmTools` | 确认工具 |
|
||||
| 44 | `getApiKeyList` | GET | `/api/datasource/api_key/list` | 密钥列表 |
|
||||
| 45 | `postApiKey` | POST | `/api/datasource/api_key` | 建密钥 |
|
||||
| 46 | `putApiKey` | PUT | `/api/datasource/api_key` | 改密钥 |
|
||||
| 47 | `deleteApiKey` | DELETE | `/api/datasource/api_key/{ids}` | 删密钥 |
|
||||
| 48 | `getApiKeyPermission` | GET | `/api/datasource/api_key/permission/{apiKeyId}` | 查权限 |
|
||||
| 49 | `postApiKeyPermissionGrantBatch` | POST | `/api/datasource/api_key/permission/grant_batch` | 批量授权 |
|
||||
| 50 | `postDatasourceSubscriptionToggle` | POST | `/api/datasource/subscription/toggle` | 订阅开关 |
|
||||
| 51 | `getBuiltinTableData` | GET | `/api/datasource/connection/builtin/table/{tableId}` | 查内置表数据 |
|
||||
| 52 | `postBuiltinTableRows` | POST | `/api/datasource/connection/builtin/table/{tableId}/rows` | 增行 |
|
||||
| 53 | `putBuiltinTableRows` | PUT | `/api/datasource/connection/builtin/table/{tableId}/rows` | 改行 |
|
||||
| 54 | `deleteBuiltinTableRows` | DELETE | `/api/datasource/connection/builtin/table/{tableId}/rows` | 删行 |
|
||||
| 55 | `getBuiltinTableExportExcel` | GET | `/api/datasource/connection/builtin/table/{tableId}/export/excel` | 导出Excel |
|
||||
| 56 | `postImportDocumentPreview` | POST | `/api/datasource/connection/{id}/import_document/preview` | 导入预览 |
|
||||
| 57 | `postImportDocumentConfirm` | POST | `/api/datasource/connection/{id}/import_document/confirm` | 导入确认 |
|
||||
| 58 | `executeSql` | POST | `/api/datasource/sqlExecutionLog/testSqlWithSchema` | 执行SQL |
|
||||
@@ -174,7 +174,7 @@
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| datasourceId | string | 是 | 数据源 ID |
|
||||
| connectionId | string | 是 | 数据源连接 ID(路径参数) |
|
||||
| databaseName | string | 是 | 目标数据库名 |
|
||||
| tableName | string | 是 | 表名(小写字母+数字+下划线) |
|
||||
| tableComment | string | 否 | 表注释 |
|
||||
@@ -198,33 +198,29 @@
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| tableId | string | 是 | 表 ID |
|
||||
| columns | array | 是 | 字段变更数组 |
|
||||
| columns[].operation | string | 是 | ADD_COLUMN / MODIFY_COLUMN / DROP_COLUMN |
|
||||
| columns[].columnName | string | 是 | 字段名 |
|
||||
| columns[].columnType | string | 否 | 字段类型(ADD/MODIFY) |
|
||||
| columns[].columnLength | int | 否 | 字段长度 |
|
||||
| columns[].isPrimaryKey | bool | 否 | 是否主键 |
|
||||
| columns[].isNullable | bool | 否 | 是否可空 |
|
||||
| columns[].columnComment | string | 否 | 字段注释 |
|
||||
| columns[].defaultValue | string | 否 | 默认值 |
|
||||
| newTableName | string | 否 | 新表名(重命名) |
|
||||
| newTableComment | string | 否 | 新表注释 |
|
||||
| connectionId | string | 是 | 数据源连接 ID(路径参数) |
|
||||
| databaseName | string | 是 | 数据库名 |
|
||||
| tableName | string | 是 | 表名 |
|
||||
| operations | array | 是 | 表结构变更操作数组 |
|
||||
| operations[].operation | string | 是 | ADD_COLUMN / DROP_COLUMN / RENAME_COLUMN / ALTER_COLUMN_TYPE / SET_NOT_NULL / DROP_NOT_NULL / SET_DEFAULT / DROP_DEFAULT |
|
||||
| operations[].column | object | 是 | 列定义(根据 operation 不同包含不同字段,如 columnName/columnType/newColumnName 等) |
|
||||
| tableComment | string | 否 | 新表注释 |
|
||||
|
||||
**返回**:修改结果
|
||||
|
||||
---
|
||||
|
||||
#### 12. `generate_table_by_description`
|
||||
- **用途**:通过自然语言描述让 AI 生成表结构
|
||||
- **用途**:通过自然语言描述让 AI 生成表结构(异步任务)
|
||||
- **对应前端**:CreateBuiltinDataSource.vue AI 生成表结构
|
||||
- **对应 API**:`postGenerateTable(data)` ✅ 已实现 — `POST /api/datasource/connection/generate_table`
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| description | string | 是 | 业务场景描述(至少6个字符) |
|
||||
| requirement | string | 是 | 业务需求描述 |
|
||||
| databaseId | int | 否 | 关联的数据库 ID |
|
||||
|
||||
**返回**:AI 生成的表结构(表名、表注释、字段列表含类型/主键/注释等)
|
||||
**返回**:异步任务信息(taskId、status),需轮询任务状态获取最终生成的表结构
|
||||
|
||||
> **场景示例**:用户说"我需要一个商城系统,管理商品、分类和用户评价",AI 返回完整的表结构设计。
|
||||
|
||||
@@ -389,6 +385,18 @@
|
||||
|
||||
---
|
||||
|
||||
#### 23.5 `revoke_api_key_permissions`
|
||||
- **用途**:撤销/删除 API 密钥已授予的权限(按权限记录 ID)
|
||||
- **对应 API**:按现有 delete 风格推测为 `DELETE /api/datasource/api_key/permission/{ids}`,需后端真机验证
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| permissionIds | array[string] | 是 | 权限记录 ID 列表。先从 `get_api_key_permissions` 获取,取 `connectionPermissions` / `databasePermissions` / `tablePermissions` 中每项的 `id` |
|
||||
|
||||
**返回**:撤销结果
|
||||
|
||||
---
|
||||
|
||||
### 🤖 技能与工具管理 (内置数据源 AI 能力)
|
||||
|
||||
#### 24. `get_skill_by_datasource`
|
||||
@@ -437,12 +445,14 @@
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| skillId | string | 是 | 技能 ID |
|
||||
| name | string | 是 | 工具名称 |
|
||||
| businessDescription | string | 是 | 业务描述 |
|
||||
| sqlTemplate | string | 是 | SQL 模板(支持 #{param} 参数占位) |
|
||||
| sqlParams | string | 否 | 参数 JSON Schema(默认空对象) |
|
||||
| resultType | string | 否 | single/list,默认 list |
|
||||
| businessScenario | string | 否 | 业务场景描述 |
|
||||
| tableIds | array | 否 | 关联的表 ID 数组 |
|
||||
| suggestions | array | 是 | SQL 工具建议数组(支持批量) |
|
||||
| suggestions[].name | string | 是 | 工具名称 |
|
||||
| suggestions[].businessDescription | string | 是 | 业务描述 |
|
||||
| suggestions[].sqlTemplate | string | 是 | SQL 模板(支持 #{param} 参数占位) |
|
||||
| suggestions[].sqlParams | string/object | 否 | 参数 JSON Schema(对象会自动序列化为字符串) |
|
||||
| suggestions[].resultType | string | 否 | single/list,默认 list |
|
||||
| suggestions[].businessScenario | string | 否 | 业务场景描述 |
|
||||
|
||||
**返回**:工具创建结果
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ from lzwcai_mcp_agile_db.server import main
|
||||
import os
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.environ["API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6IjJiMDY0YzMzLTBiZWYtNDU0NC04NWY1LTRmNTFiOGMxMmI5NSJ9.Uv599TvlQvlTlwrnZGo3Tl2eLAvM0ldE9vpMI5jHxbTf4_tVSRA60rUNIV4LBiw6pt1r_xIi7aFcTRE2PeN5sg"
|
||||
os.environ["backendBaseUrl"] = "https://dempdemo.lzwcai.com"
|
||||
os.environ["API_KEY"] = "Bearer eyJhbGciOiJIUzUxMiJ9.eyJ0b2tlbl90eXBlIjoiTE9HSU4iLCJsb2dpbl91c2VyX2tleSI6ImNlMDAwYjA4LWU0YTYtNGM2MS1hNzJiLWI3NTlmNmY1N2Q4NCJ9.jiNmGQZfL4-nSIFrLuaCt7mT5zj0FOojAVkLeHwPOroI5jBxodrCe1PSwGO1OHq5Ztb0tLEVZw2FFVj0OlTceQ"
|
||||
os.environ["backendBaseUrl"] = "http://192.168.2.236:8088"
|
||||
main()
|
||||
|
||||
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "lzwcai-mcp-agile-db"
|
||||
version = "0.1.3"
|
||||
version = "0.1.7"
|
||||
description = "MCP server for database management platform with 33 tools for datasource, table, data, API key, and skill management"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
Reference in New Issue
Block a user