Build lightweight AI agent admin
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
数据源模块
|
||||
"""
|
||||
from core.data_source.api import router
|
||||
|
||||
__all__ = ['router']
|
||||
@@ -0,0 +1,419 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Data Source API - 数据源接口
|
||||
提供数据源的增删改查、执行、测试等功能
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from typing import List, Any, Dict, Optional
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.database import get_db
|
||||
from core.application.model import Application
|
||||
from app.config import settings
|
||||
from app.base_schema import PaginatedResponse, ResponseModel
|
||||
from core.data_source.model import DataSource
|
||||
from core.data_source.schema import (
|
||||
DataSourceCreate,
|
||||
DataSourceUpdate,
|
||||
DataSourceResponse,
|
||||
DataSourceSimpleOut,
|
||||
DataSourcePreviewRequest,
|
||||
DataSourceExecuteRequest,
|
||||
DataSourceTestRequest,
|
||||
DataSourceCopyRequest,
|
||||
DataSourceImportCheckIn,
|
||||
DataSourceImportCheckOut,
|
||||
DataSourceImportIn,
|
||||
AIGenerateSqlRequest,
|
||||
AIGenerateSqlResponse,
|
||||
)
|
||||
from core.data_source.service import DataSourceService
|
||||
from core.data_source.import_export import (
|
||||
DataSourceImportExportException,
|
||||
export_config as export_data_source_config,
|
||||
check_import as check_data_source_import,
|
||||
import_config as import_data_source_config,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(prefix="/data-source", tags=["数据源管理"])
|
||||
|
||||
|
||||
# ============ 静态路径接口(必须放在动态路径之前) ============
|
||||
|
||||
@router.get("", response_model=PaginatedResponse[DataSourceResponse], summary="获取数据源列表")
|
||||
async def list_data_source(
|
||||
page: int = Query(default=1, ge=1, description="页码"),
|
||||
page_size: int = Query(default=settings.PAGE_SIZE, ge=1, le=settings.PAGE_MAX_SIZE, alias="pageSize", description="每页数量"),
|
||||
application_id: Optional[str] = Query(default=None, alias="applicationId", description="所属应用ID"),
|
||||
name: str = Query(default=None, description="名称(模糊查询)"),
|
||||
code: str = Query(default=None, description="编码(模糊查询)"),
|
||||
source_type: str = Query(default=None, alias="sourceType", description="类型"),
|
||||
status: bool = Query(default=None, description="状态"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取数据源列表(分页,自动应用数据权限)"""
|
||||
items, total = await DataSourceService.get_list_with_data_scope(
|
||||
db, page=page, page_size=page_size,
|
||||
application_id=application_id,
|
||||
name=name, code=code, source_type=source_type, status=status,
|
||||
)
|
||||
|
||||
# 批量查询应用名称
|
||||
app_ids = list({item.application_id for item in items if item.application_id})
|
||||
app_name_map = {}
|
||||
if app_ids:
|
||||
app_result = await db.execute(
|
||||
select(Application.id, Application.name).where(Application.id.in_(app_ids))
|
||||
)
|
||||
app_name_map = {row.id: row.name for row in app_result}
|
||||
|
||||
# 构建响应,附加 application_name
|
||||
result_items = []
|
||||
for item in items:
|
||||
item_dict = DataSourceResponse.model_validate(item).model_dump()
|
||||
item_dict["application_name"] = app_name_map.get(item.application_id, "")
|
||||
result_items.append(item_dict)
|
||||
|
||||
return PaginatedResponse(items=result_items, total=total)
|
||||
|
||||
|
||||
@router.post("", response_model=DataSourceResponse, summary="创建数据源")
|
||||
async def create_data_source(
|
||||
data: DataSourceCreate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""创建数据源"""
|
||||
# 检查编码是否已存在
|
||||
if await DataSourceService.check_code_exists(db, data.code):
|
||||
raise HTTPException(status_code=400, detail=f"编码已存在: {data.code}")
|
||||
|
||||
source = await DataSourceService.create(db, data.model_dump())
|
||||
await db.commit()
|
||||
logger.info(f"数据源已创建: {source.code}")
|
||||
return source
|
||||
|
||||
|
||||
@router.get("/get/all", response_model=List[DataSourceSimpleOut], summary="获取所有数据源")
|
||||
async def list_all_data_source(
|
||||
application_id: Optional[str] = Query(default=None, alias="applicationId", description="所属应用ID"),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取所有数据源(不分页,用于下拉选择)"""
|
||||
items = await DataSourceService.get_all(db, application_id=application_id)
|
||||
return items
|
||||
|
||||
|
||||
@router.post("/ai/generate-sql", response_model=AIGenerateSqlResponse, summary="AI 生成 SQL")
|
||||
async def ai_generate_sql(
|
||||
body: AIGenerateSqlRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""
|
||||
使用 AI 生成 SQL 语句
|
||||
|
||||
根据用户的自然语言描述,结合选中的表结构和表关系,自动生成 SQL 查询语句。
|
||||
"""
|
||||
try:
|
||||
# 将表关系转换为字典格式
|
||||
table_relations = [rel.model_dump() for rel in body.table_relations] if body.table_relations else []
|
||||
|
||||
result = await DataSourceService.ai_generate_sql(
|
||||
db=db,
|
||||
user_question=body.user_question,
|
||||
db_connection=body.db_connection,
|
||||
database=body.database,
|
||||
schema_name=body.schema_name,
|
||||
selected_tables=body.selected_tables,
|
||||
table_fields=body.table_fields,
|
||||
table_relations=table_relations,
|
||||
include_table_relations=body.include_table_relations,
|
||||
model_id=body.model_id,
|
||||
)
|
||||
return result
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"AI 生成 SQL 失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"生成失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/test", summary="测试数据源配置")
|
||||
async def test_data_source(
|
||||
body: DataSourceTestRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""测试数据源配置(不保存,直接执行)"""
|
||||
try:
|
||||
config = {
|
||||
'source_type': body.source_type,
|
||||
'api_url': body.api_url,
|
||||
'api_method': body.api_method,
|
||||
'api_headers': body.api_headers,
|
||||
'api_query_params': body.api_query_params,
|
||||
'api_body_type': body.api_body_type,
|
||||
'api_body': body.api_body,
|
||||
'api_content_type': body.api_content_type,
|
||||
'api_timeout': body.api_timeout,
|
||||
'api_data_path': body.api_data_path,
|
||||
'api_auth_type': body.api_auth_type,
|
||||
'api_auth_config': body.api_auth_config,
|
||||
'api_retry_count': body.api_retry_count,
|
||||
'api_retry_interval': body.api_retry_interval,
|
||||
'api_success_condition': body.api_success_condition,
|
||||
'api_proxy': body.api_proxy,
|
||||
'api_follow_redirects': body.api_follow_redirects,
|
||||
'api_verify_ssl': body.api_verify_ssl,
|
||||
'sql_content': body.sql_content,
|
||||
'db_connection': body.db_connection,
|
||||
'static_data': body.static_data,
|
||||
'params_def': body.params_def,
|
||||
'result_type': body.result_type,
|
||||
'tree_config': body.tree_config,
|
||||
'field_mapping': body.field_mapping,
|
||||
'chart_config': body.chart_config,
|
||||
}
|
||||
|
||||
data = await DataSourceService.execute_temp(db, config, body.params)
|
||||
|
||||
total = len(data) if isinstance(data, list) else 1
|
||||
|
||||
return {
|
||||
'data': data,
|
||||
'total': total,
|
||||
'limited': DataSourceService.MAX_ROWS_TEST,
|
||||
'success': True
|
||||
}
|
||||
except ValueError as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"数据源测试失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"测试失败: {str(e)}")
|
||||
|
||||
|
||||
@router.get("/check-code/{code}", summary="检查编码是否可用")
|
||||
async def check_code_available(
|
||||
code: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""检查数据源编码是否可用"""
|
||||
exists = await DataSourceService.check_code_exists(db, code)
|
||||
return {'available': not exists}
|
||||
|
||||
|
||||
@router.post("/import/check", response_model=DataSourceImportCheckOut, summary="导入预检查")
|
||||
async def check_import_data_source(
|
||||
data: DataSourceImportCheckIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导入预检查:检查数据源编码是否冲突"""
|
||||
try:
|
||||
return await check_data_source_import(db, data.code)
|
||||
except DataSourceImportExportException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/import", response_model=DataSourceResponse, summary="导入数据源配置")
|
||||
async def import_data_source(
|
||||
data: DataSourceImportIn,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导入数据源配置"""
|
||||
try:
|
||||
source = await import_data_source_config(db, data.model_dump())
|
||||
await db.commit()
|
||||
return source
|
||||
except DataSourceImportExportException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/code/{code}", response_model=DataSourceResponse, summary="根据编码获取数据源详情")
|
||||
async def get_data_source_by_code(
|
||||
code: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""根据编码获取数据源详情(包含参数定义)"""
|
||||
source = await DataSourceService.get_by_code(db, code)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail=f"数据源不存在: {code}")
|
||||
return source
|
||||
|
||||
|
||||
# ============ 执行接口 ============
|
||||
|
||||
@router.get("/execute/{code}", summary="执行数据源(GET)")
|
||||
async def execute_data_source_get(
|
||||
request: Request,
|
||||
code: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""根据编码执行数据源获取数据(GET 方式)"""
|
||||
# 获取所有查询参数
|
||||
params = dict(request.query_params)
|
||||
|
||||
try:
|
||||
data = await DataSourceService.execute(db, code, params)
|
||||
return {'data': data}
|
||||
except ValueError as e:
|
||||
if "不存在" in str(e):
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"数据源执行失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"执行失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/execute/{code}", summary="执行数据源(POST)")
|
||||
async def execute_data_source_post(
|
||||
code: str,
|
||||
body: DataSourceExecuteRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""根据编码执行数据源获取数据(POST 方式)"""
|
||||
try:
|
||||
data = await DataSourceService.execute(db, code, body.params)
|
||||
return {'data': data}
|
||||
except ValueError as e:
|
||||
if "不存在" in str(e):
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"数据源执行失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"执行失败: {str(e)}")
|
||||
|
||||
|
||||
# ============ 动态路径接口(必须放在静态路径之后) ============
|
||||
|
||||
@router.get("/{source_id}", response_model=DataSourceResponse, summary="获取数据源详情")
|
||||
async def get_data_source(
|
||||
source_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取数据源详情"""
|
||||
source = await DataSourceService.get_by_id(db, source_id)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
return source
|
||||
|
||||
|
||||
@router.get("/{source_id}/export", summary="导出数据源配置")
|
||||
async def export_data_source(
|
||||
source_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""导出数据源配置为 JSON"""
|
||||
try:
|
||||
config = await export_data_source_config(db, source_id)
|
||||
content = json.dumps(config, ensure_ascii=False, indent=2)
|
||||
return StreamingResponse(
|
||||
iter([content]),
|
||||
media_type="application/json",
|
||||
headers={
|
||||
"Content-Disposition": f'attachment; filename="{config["code"]}.json"'
|
||||
},
|
||||
)
|
||||
except DataSourceImportExportException as e:
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
|
||||
|
||||
@router.put("/{source_id}", response_model=DataSourceResponse, summary="更新数据源")
|
||||
async def update_data_source(
|
||||
source_id: str,
|
||||
data: DataSourceUpdate,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""更新数据源"""
|
||||
# 如果更新了编码,检查新编码是否已存在
|
||||
if data.code:
|
||||
if await DataSourceService.check_code_exists(db, data.code, exclude_id=source_id):
|
||||
raise HTTPException(status_code=400, detail=f"编码已存在: {data.code}")
|
||||
|
||||
source = await DataSourceService.update(db, source_id, data.model_dump(exclude_unset=True))
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
await db.commit()
|
||||
logger.info(f"数据源已更新: {source.code}")
|
||||
return source
|
||||
|
||||
|
||||
@router.delete("/{source_id}", response_model=ResponseModel, summary="删除数据源")
|
||||
async def delete_data_source(
|
||||
source_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""删除数据源"""
|
||||
success = await DataSourceService.delete(db, source_id)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
await db.commit()
|
||||
return ResponseModel(message="删除成功")
|
||||
|
||||
|
||||
# ============ 预览和其他接口 ============
|
||||
|
||||
@router.post("/{source_id}/preview", summary="预览数据源数据")
|
||||
async def preview_data_source(
|
||||
source_id: str,
|
||||
body: DataSourcePreviewRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""预览数据源数据(用于调试)"""
|
||||
try:
|
||||
data = await DataSourceService.execute_by_id(db, source_id, body.params)
|
||||
|
||||
# 限制返回数量
|
||||
if isinstance(data, list) and len(data) > body.limit:
|
||||
data = data[:body.limit]
|
||||
|
||||
return {
|
||||
'data': data,
|
||||
'total': len(data) if isinstance(data, list) else 1,
|
||||
'limited': body.limit
|
||||
}
|
||||
except ValueError as e:
|
||||
if "不存在" in str(e):
|
||||
raise HTTPException(status_code=404, detail=str(e))
|
||||
raise HTTPException(status_code=400, detail=str(e))
|
||||
except Exception as e:
|
||||
logger.error(f"数据源预览失败: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"预览失败: {str(e)}")
|
||||
|
||||
|
||||
@router.post("/{source_id}/copy", response_model=DataSourceResponse, summary="复制数据源")
|
||||
async def copy_data_source(
|
||||
source_id: str,
|
||||
body: DataSourceCopyRequest,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""复制数据源"""
|
||||
# 检查新编码是否已存在
|
||||
if await DataSourceService.check_code_exists(db, body.new_code):
|
||||
raise HTTPException(status_code=400, detail=f"编码已存在: {body.new_code}")
|
||||
|
||||
source = await DataSourceService.copy(db, source_id, body.new_code, body.new_name)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
await db.commit()
|
||||
logger.info(f"数据源已复制: {body.new_code}")
|
||||
return source
|
||||
|
||||
|
||||
@router.post("/{source_id}/clear-cache", response_model=ResponseModel, summary="清除数据源缓存")
|
||||
async def clear_data_source_cache(
|
||||
source_id: str,
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""清除数据源缓存"""
|
||||
source = await DataSourceService.get_by_id(db, source_id)
|
||||
if not source:
|
||||
raise HTTPException(status_code=404, detail="数据源不存在")
|
||||
|
||||
await DataSourceService.clear_cache(source.code)
|
||||
return ResponseModel(message=f"缓存已清除: {source.code}")
|
||||
@@ -0,0 +1,149 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
数据源导入/导出
|
||||
"""
|
||||
import logging
|
||||
from typing import Any, Dict
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from core.data_source.model import DataSource
|
||||
from core.data_source.service import DataSourceService
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
EXPORT_FIELD_KEYS = [
|
||||
"name", "code", "source_type", "description", "status",
|
||||
"api_url", "api_method", "api_headers", "api_query_params", "api_body_type",
|
||||
"api_body", "api_content_type", "api_timeout", "api_data_path",
|
||||
"api_auth_type", "api_auth_config", "api_retry_count", "api_retry_interval",
|
||||
"api_success_condition", "api_proxy", "api_follow_redirects", "api_verify_ssl",
|
||||
"sql_content", "db_connection", "static_data", "params",
|
||||
"result_type", "tree_config", "field_mapping", "chart_config",
|
||||
"cache_enabled", "cache_ttl",
|
||||
]
|
||||
|
||||
|
||||
class DataSourceImportExportException(Exception):
|
||||
"""数据源导入导出异常"""
|
||||
pass
|
||||
|
||||
|
||||
def _source_to_export_dict(source: DataSource) -> Dict[str, Any]:
|
||||
"""将数据源转为可导出配置(不含 id、统计字段)"""
|
||||
return {
|
||||
"name": source.name,
|
||||
"code": source.code,
|
||||
"source_type": source.source_type or "static",
|
||||
"description": source.description or "",
|
||||
"status": source.status if source.status is not None else True,
|
||||
"api_url": source.api_url or "",
|
||||
"api_method": source.api_method or "GET",
|
||||
"api_headers": source.api_headers or {},
|
||||
"api_query_params": source.api_query_params or [],
|
||||
"api_body_type": source.api_body_type or "none",
|
||||
"api_body": source.api_body or {},
|
||||
"api_content_type": source.api_content_type or "",
|
||||
"api_timeout": source.api_timeout or 30,
|
||||
"api_data_path": source.api_data_path or "",
|
||||
"api_auth_type": source.api_auth_type or "none",
|
||||
"api_auth_config": source.api_auth_config or {},
|
||||
"api_retry_count": source.api_retry_count or 0,
|
||||
"api_retry_interval": source.api_retry_interval or 1,
|
||||
"api_success_condition": source.api_success_condition or {},
|
||||
"api_proxy": source.api_proxy or "",
|
||||
"api_follow_redirects": (
|
||||
source.api_follow_redirects if source.api_follow_redirects is not None else True
|
||||
),
|
||||
"api_verify_ssl": (
|
||||
source.api_verify_ssl if source.api_verify_ssl is not None else True
|
||||
),
|
||||
"sql_content": source.sql_content or "",
|
||||
"db_connection": source.db_connection or "default",
|
||||
"static_data": source.static_data or [],
|
||||
"params": source.params or [],
|
||||
"result_type": source.result_type or "list",
|
||||
"tree_config": source.tree_config or {},
|
||||
"field_mapping": source.field_mapping or {},
|
||||
"chart_config": source.chart_config or {},
|
||||
"cache_enabled": source.cache_enabled or False,
|
||||
"cache_ttl": source.cache_ttl or 300,
|
||||
}
|
||||
|
||||
|
||||
def _build_import_payload(data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""从导入 JSON 构建创建参数"""
|
||||
defaults = {
|
||||
"source_type": "static",
|
||||
"description": "",
|
||||
"status": True,
|
||||
"api_url": "",
|
||||
"api_method": "GET",
|
||||
"api_headers": {},
|
||||
"api_query_params": [],
|
||||
"api_body_type": "none",
|
||||
"api_body": {},
|
||||
"api_content_type": "",
|
||||
"api_timeout": 30,
|
||||
"api_data_path": "",
|
||||
"api_auth_type": "none",
|
||||
"api_auth_config": {},
|
||||
"api_retry_count": 0,
|
||||
"api_retry_interval": 1,
|
||||
"api_success_condition": {},
|
||||
"api_proxy": "",
|
||||
"api_follow_redirects": True,
|
||||
"api_verify_ssl": True,
|
||||
"sql_content": "",
|
||||
"db_connection": "default",
|
||||
"static_data": [],
|
||||
"params": [],
|
||||
"result_type": "list",
|
||||
"tree_config": {},
|
||||
"field_mapping": {},
|
||||
"chart_config": {},
|
||||
"cache_enabled": False,
|
||||
"cache_ttl": 300,
|
||||
}
|
||||
payload = {**defaults}
|
||||
for key in EXPORT_FIELD_KEYS:
|
||||
if key in data and data[key] is not None:
|
||||
payload[key] = data[key]
|
||||
payload["application_id"] = data.get("application_id")
|
||||
return payload
|
||||
|
||||
|
||||
async def export_config(db: AsyncSession, source_id: str) -> Dict[str, Any]:
|
||||
"""导出数据源配置"""
|
||||
source = await DataSourceService.get_by_id(db, source_id)
|
||||
if not source:
|
||||
raise DataSourceImportExportException("数据源不存在")
|
||||
return _source_to_export_dict(source)
|
||||
|
||||
|
||||
async def check_import(db: AsyncSession, code: str) -> Dict[str, Any]:
|
||||
"""导入预检查:编码是否冲突"""
|
||||
code_exists = False
|
||||
if code:
|
||||
code_exists = await DataSourceService.check_code_exists(db, code)
|
||||
return {
|
||||
"code_exists": code_exists,
|
||||
"can_import": not code_exists,
|
||||
}
|
||||
|
||||
|
||||
async def import_config(db: AsyncSession, data: Dict[str, Any]) -> DataSource:
|
||||
"""导入数据源配置"""
|
||||
if not data.get("name"):
|
||||
raise DataSourceImportExportException("缺少必要字段: name")
|
||||
if not data.get("code"):
|
||||
raise DataSourceImportExportException("缺少必要字段: code")
|
||||
|
||||
if await DataSourceService.check_code_exists(db, data["code"]):
|
||||
raise DataSourceImportExportException(f"数据源编码已存在: {data['code']}")
|
||||
|
||||
create_data = _build_import_payload(data)
|
||||
source = await DataSourceService.create(db, create_data)
|
||||
logger.info("数据源导入成功: %s", source.code)
|
||||
return source
|
||||
@@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Data Source Model - 数据源模型
|
||||
用于管理系统数据源配置,支持 API、SQL、静态数据等多种类型
|
||||
"""
|
||||
from sqlalchemy import Column, String, Text, Boolean, Integer, JSON
|
||||
|
||||
from app.base_model import BaseModel
|
||||
|
||||
|
||||
class DataSource(BaseModel):
|
||||
"""
|
||||
数据源模型
|
||||
|
||||
支持三种数据源类型:
|
||||
1. API - 调用外部或内部 API 接口
|
||||
2. SQL - 执行 SQL 查询(只读)
|
||||
3. Static - 静态数据
|
||||
"""
|
||||
__tablename__ = "core_data_source"
|
||||
|
||||
# 所属应用(逻辑外键关联 core_application)
|
||||
application_id = Column(String(21), nullable=True, index=True, comment="所属应用ID")
|
||||
|
||||
# 基本信息
|
||||
name = Column(String(100), nullable=False, comment="数据源名称")
|
||||
code = Column(String(50), unique=True, index=True, nullable=False, comment="数据源编码(唯一标识)")
|
||||
source_type = Column(String(20), default='static', comment="数据源类型: api/sql/static")
|
||||
description = Column(Text, default='', comment="描述说明")
|
||||
status = Column(Boolean, default=True, comment="是否启用")
|
||||
|
||||
# ===== API 配置 =====
|
||||
api_url = Column(String(500), default='', comment="API地址,支持 {param} 占位符")
|
||||
api_method = Column(String(10), default='GET', comment="请求方法: GET/POST/PUT/DELETE/PATCH")
|
||||
api_headers = Column(JSON, default=dict, comment="请求头配置")
|
||||
api_query_params = Column(JSON, default=list, comment="Query参数列表 [{key, value, description, enabled}]")
|
||||
api_body_type = Column(String(20), default='none', comment="请求体类型: none/json/form-data/x-www-form-urlencoded/raw")
|
||||
api_body = Column(JSON, default=dict, comment="请求体模板")
|
||||
api_content_type = Column(String(50), default='', comment="Content-Type,raw模式时使用")
|
||||
api_timeout = Column(Integer, default=30, comment="请求超时时间(秒)")
|
||||
api_data_path = Column(String(100), default='', comment="响应数据路径,如 data.list")
|
||||
|
||||
# ===== API 认证配置 =====
|
||||
api_auth_type = Column(String(20), default='none', comment="认证类型: none/bearer_token/basic_auth/api_key")
|
||||
api_auth_config = Column(JSON, default=dict, comment="认证配置 {token, username, password, key_name, key_value, key_position}")
|
||||
|
||||
# ===== API 高级配置 =====
|
||||
api_retry_count = Column(Integer, default=0, comment="重试次数(0表示不重试)")
|
||||
api_retry_interval = Column(Integer, default=1, comment="重试间隔(秒)")
|
||||
api_success_condition = Column(JSON, default=dict, comment="成功条件 {status_codes, field_path, field_value}")
|
||||
api_proxy = Column(String(200), default='', comment="代理地址")
|
||||
api_follow_redirects = Column(Boolean, default=True, comment="是否跟随重定向")
|
||||
api_verify_ssl = Column(Boolean, default=True, comment="是否验证SSL证书")
|
||||
|
||||
# ===== SQL 配置 =====
|
||||
sql_content = Column(Text, default='', comment="SQL语句,使用 :param 作为参数占位符")
|
||||
db_connection = Column(String(50), default='default', comment="数据库连接名称")
|
||||
|
||||
# ===== 静态数据 =====
|
||||
static_data = Column(JSON, default=list, comment="静态数据(JSON 数组)")
|
||||
|
||||
# ===== 参数定义 =====
|
||||
params = Column(JSON, default=list, comment="参数定义列表")
|
||||
|
||||
# ===== 结果处理 =====
|
||||
result_type = Column(String(20), default='list', comment="结果类型: list/tree/object/value/chart-*")
|
||||
tree_config = Column(JSON, default=dict, comment="树形转换配置")
|
||||
field_mapping = Column(JSON, default=dict, comment="字段映射配置")
|
||||
chart_config = Column(JSON, default=dict, comment="图表配置")
|
||||
|
||||
# ===== 缓存配置 =====
|
||||
cache_enabled = Column(Boolean, default=False, comment="是否启用缓存")
|
||||
cache_ttl = Column(Integer, default=300, comment="缓存时间(秒)")
|
||||
|
||||
def __repr__(self):
|
||||
return f"<DataSource {self.name} ({self.code})>"
|
||||
@@ -0,0 +1,238 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Data Source Schema - 数据源数据验证模式
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, List, Any, Dict
|
||||
|
||||
from pydantic import BaseModel, ConfigDict, Field
|
||||
|
||||
from app.base_schema import CSTDatetime
|
||||
|
||||
|
||||
class DataSourceBase(BaseModel):
|
||||
"""数据源基础Schema"""
|
||||
application_id: Optional[str] = Field(None, description="所属应用ID")
|
||||
name: str = Field(..., description="数据源名称")
|
||||
code: str = Field(..., pattern=r"^[a-zA-Z][a-zA-Z0-9_]*$", description="数据源编码(字母开头,只能包含字母、数字和下划线)")
|
||||
source_type: str = Field(default='static', description="数据源类型: api/sql/static")
|
||||
description: str = Field(default='', description="描述说明")
|
||||
status: bool = Field(default=True, description="是否启用")
|
||||
|
||||
# API 配置
|
||||
api_url: str = Field(default='', description="API地址")
|
||||
api_method: str = Field(default='GET', description="请求方法: GET/POST/PUT/DELETE/PATCH")
|
||||
api_headers: Dict[str, str] = Field(default_factory=dict, description="请求头")
|
||||
api_query_params: List[Dict[str, Any]] = Field(default_factory=list, description="Query参数列表")
|
||||
api_body_type: str = Field(default='none', description="请求体类型: none/json/form-data/x-www-form-urlencoded/raw")
|
||||
api_body: Dict[str, Any] = Field(default_factory=dict, description="请求体模板")
|
||||
api_content_type: str = Field(default='', description="Content-Type,raw模式时使用")
|
||||
api_timeout: int = Field(default=30, description="超时时间")
|
||||
api_data_path: str = Field(default='', description="响应数据路径")
|
||||
|
||||
# API 认证配置
|
||||
api_auth_type: str = Field(default='none', description="认证类型: none/bearer_token/basic_auth/api_key")
|
||||
api_auth_config: Dict[str, Any] = Field(default_factory=dict, description="认证配置")
|
||||
|
||||
# API 高级配置
|
||||
api_retry_count: int = Field(default=0, description="重试次数")
|
||||
api_retry_interval: int = Field(default=1, description="重试间隔(秒)")
|
||||
api_success_condition: Dict[str, Any] = Field(default_factory=dict, description="成功条件")
|
||||
api_proxy: str = Field(default='', description="代理地址")
|
||||
api_follow_redirects: bool = Field(default=True, description="是否跟随重定向")
|
||||
api_verify_ssl: bool = Field(default=True, description="是否验证SSL证书")
|
||||
|
||||
# SQL 配置
|
||||
sql_content: str = Field(default='', description="SQL语句")
|
||||
db_connection: str = Field(default='default', description="数据库连接")
|
||||
|
||||
# 静态数据
|
||||
static_data: List[Any] = Field(default_factory=list, description="静态数据")
|
||||
|
||||
# 参数定义
|
||||
params: List[Dict[str, Any]] = Field(default_factory=list, description="参数定义")
|
||||
|
||||
# 结果处理
|
||||
result_type: str = Field(default='list', description="结果类型")
|
||||
tree_config: Dict[str, Any] = Field(default_factory=dict, description="树形配置")
|
||||
field_mapping: Dict[str, str] = Field(default_factory=dict, description="字段映射")
|
||||
chart_config: Dict[str, Any] = Field(default_factory=dict, description="图表配置")
|
||||
|
||||
# 缓存配置
|
||||
cache_enabled: bool = Field(default=False, description="是否启用缓存")
|
||||
cache_ttl: int = Field(default=300, description="缓存时间")
|
||||
|
||||
|
||||
class DataSourceCreate(DataSourceBase):
|
||||
"""数据源创建Schema"""
|
||||
pass
|
||||
|
||||
|
||||
class DataSourceUpdate(BaseModel):
|
||||
"""数据源更新Schema"""
|
||||
application_id: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
code: Optional[str] = Field(None, pattern=r"^[a-zA-Z][a-zA-Z0-9_]*$", description="数据源编码(字母开头,只能包含字母、数字和下划线)")
|
||||
source_type: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
status: Optional[bool] = None
|
||||
|
||||
api_url: Optional[str] = None
|
||||
api_method: Optional[str] = None
|
||||
api_headers: Optional[Dict[str, str]] = None
|
||||
api_query_params: Optional[List[Dict[str, Any]]] = None
|
||||
api_body_type: Optional[str] = None
|
||||
api_body: Optional[Dict[str, Any]] = None
|
||||
api_content_type: Optional[str] = None
|
||||
api_timeout: Optional[int] = None
|
||||
api_data_path: Optional[str] = None
|
||||
api_auth_type: Optional[str] = None
|
||||
api_auth_config: Optional[Dict[str, Any]] = None
|
||||
api_retry_count: Optional[int] = None
|
||||
api_retry_interval: Optional[int] = None
|
||||
api_success_condition: Optional[Dict[str, Any]] = None
|
||||
api_proxy: Optional[str] = None
|
||||
api_follow_redirects: Optional[bool] = None
|
||||
api_verify_ssl: Optional[bool] = None
|
||||
|
||||
sql_content: Optional[str] = None
|
||||
db_connection: Optional[str] = None
|
||||
|
||||
static_data: Optional[List[Any]] = None
|
||||
params: Optional[List[Dict[str, Any]]] = None
|
||||
|
||||
result_type: Optional[str] = None
|
||||
tree_config: Optional[Dict[str, Any]] = None
|
||||
field_mapping: Optional[Dict[str, str]] = None
|
||||
chart_config: Optional[Dict[str, Any]] = None
|
||||
|
||||
cache_enabled: Optional[bool] = None
|
||||
cache_ttl: Optional[int] = None
|
||||
|
||||
|
||||
class DataSourceResponse(DataSourceBase):
|
||||
"""数据源响应Schema"""
|
||||
id: str
|
||||
sort: int = 0
|
||||
is_deleted: bool = False
|
||||
sys_create_datetime: Optional[CSTDatetime] = None
|
||||
sys_update_datetime: Optional[CSTDatetime] = None
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DataSourceSimpleOut(BaseModel):
|
||||
"""数据源简单输出(用于下拉选择)"""
|
||||
id: str
|
||||
application_id: Optional[str] = None
|
||||
name: str
|
||||
code: str
|
||||
source_type: str
|
||||
result_type: str = "list"
|
||||
description: str = ""
|
||||
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
|
||||
class DataSourcePreviewRequest(BaseModel):
|
||||
"""数据源预览请求"""
|
||||
params: Dict[str, Any] = Field(default_factory=dict)
|
||||
limit: int = Field(default=100, ge=1, le=1000)
|
||||
|
||||
|
||||
class DataSourceExecuteRequest(BaseModel):
|
||||
"""数据源执行请求"""
|
||||
params: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class DataSourceTestRequest(BaseModel):
|
||||
"""数据源测试请求(临时配置)"""
|
||||
source_type: str
|
||||
# API 配置
|
||||
api_url: str = ""
|
||||
api_method: str = "GET"
|
||||
api_headers: Dict[str, str] = Field(default_factory=dict)
|
||||
api_query_params: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
api_body_type: str = "none"
|
||||
api_body: Dict[str, Any] = Field(default_factory=dict)
|
||||
api_content_type: str = ""
|
||||
api_timeout: int = 30
|
||||
api_data_path: str = ""
|
||||
# API 认证
|
||||
api_auth_type: str = "none"
|
||||
api_auth_config: Dict[str, Any] = Field(default_factory=dict)
|
||||
# API 高级
|
||||
api_retry_count: int = 0
|
||||
api_retry_interval: int = 1
|
||||
api_success_condition: Dict[str, Any] = Field(default_factory=dict)
|
||||
api_proxy: str = ""
|
||||
api_follow_redirects: bool = True
|
||||
api_verify_ssl: bool = True
|
||||
# SQL 配置
|
||||
sql_content: str = ""
|
||||
db_connection: str = "default"
|
||||
# 静态数据
|
||||
static_data: List[Any] = Field(default_factory=list)
|
||||
# 参数
|
||||
params_def: List[Dict[str, Any]] = Field(default_factory=list)
|
||||
params: Dict[str, Any] = Field(default_factory=dict)
|
||||
# 结果处理
|
||||
result_type: str = "list"
|
||||
tree_config: Dict[str, Any] = Field(default_factory=dict)
|
||||
field_mapping: Dict[str, str] = Field(default_factory=dict)
|
||||
chart_config: Dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class DataSourceCopyRequest(BaseModel):
|
||||
"""数据源复制请求"""
|
||||
new_code: str = Field(..., description="新编码")
|
||||
new_name: str = Field(default="", description="新名称")
|
||||
|
||||
|
||||
class DataSourceImportCheckIn(BaseModel):
|
||||
"""数据源导入预检查请求"""
|
||||
code: str = Field(..., description="数据源编码")
|
||||
|
||||
|
||||
class DataSourceImportCheckOut(BaseModel):
|
||||
"""数据源导入预检查结果"""
|
||||
code_exists: bool = Field(..., description="数据源编码是否已存在")
|
||||
can_import: bool = Field(..., description="是否可以直接导入(编码不冲突)")
|
||||
|
||||
|
||||
class DataSourceImportIn(DataSourceBase):
|
||||
"""数据源配置导入"""
|
||||
pass
|
||||
|
||||
|
||||
class TableRelation(BaseModel):
|
||||
"""表关系定义"""
|
||||
id: str = Field(default="", description="关系ID")
|
||||
sourceTable: str = Field(..., description="源表名")
|
||||
sourceField: str = Field(..., description="源字段名")
|
||||
targetTable: str = Field(..., description="目标表名")
|
||||
targetField: str = Field(..., description="目标字段名")
|
||||
relationType: str = Field(default="many-to-one", description="关系类型")
|
||||
|
||||
|
||||
class AIGenerateSqlRequest(BaseModel):
|
||||
"""AI 生成 SQL 请求"""
|
||||
model_config = ConfigDict(protected_namespaces=())
|
||||
|
||||
user_question: str = Field(..., description="用户问题(自然语言描述)")
|
||||
db_connection: str = Field(default="default", description="数据库连接名称")
|
||||
database: str = Field(default="", description="数据库名")
|
||||
schema_name: str = Field(default="", description="Schema 名称")
|
||||
selected_tables: List[str] = Field(default_factory=list, description="选中的表名列表")
|
||||
table_fields: Dict[str, List[Dict[str, Any]]] = Field(default_factory=dict, description="表字段信息")
|
||||
table_relations: List[TableRelation] = Field(default_factory=list, description="表关系列表")
|
||||
include_table_relations: bool = Field(default=True, description="是否包含表关系信息")
|
||||
model_id: str = Field(..., description="LLM 模型 ID")
|
||||
|
||||
|
||||
class AIGenerateSqlResponse(BaseModel):
|
||||
"""AI 生成 SQL 响应"""
|
||||
sql: str = Field(..., description="生成的 SQL 语句")
|
||||
thought: str = Field(default="", description="生成思路")
|
||||
params: List[Dict[str, Any]] = Field(default_factory=list, description="推荐的参数定义")
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user