sql-assistant 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- sql_assistant/__init__.py +3 -0
- sql_assistant/api/__init__.py +1 -0
- sql_assistant/api/backup.py +116 -0
- sql_assistant/api/config.py +183 -0
- sql_assistant/api/conversation.py +71 -0
- sql_assistant/api/dependencies.py +22 -0
- sql_assistant/api/history.py +61 -0
- sql_assistant/api/models.py +221 -0
- sql_assistant/api/query.py +275 -0
- sql_assistant/api/routes.py +19 -0
- sql_assistant/api/schema.py +21 -0
- sql_assistant/config.py +144 -0
- sql_assistant/database/__init__.py +1 -0
- sql_assistant/database/backup.py +568 -0
- sql_assistant/database/connectors/__init__.py +1 -0
- sql_assistant/database/connectors/base.py +185 -0
- sql_assistant/database/connectors/exceptions.py +88 -0
- sql_assistant/database/connectors/mongodb.py +194 -0
- sql_assistant/database/connectors/mysql.py +110 -0
- sql_assistant/database/connectors/postgresql.py +133 -0
- sql_assistant/database/connectors/redis.py +132 -0
- sql_assistant/database/connectors/sqlserver.py +140 -0
- sql_assistant/database/history.py +290 -0
- sql_assistant/database/manager.py +178 -0
- sql_assistant/database/security.py +230 -0
- sql_assistant/llm/__init__.py +1 -0
- sql_assistant/llm/base.py +28 -0
- sql_assistant/llm/exceptions.py +96 -0
- sql_assistant/llm/manager.py +82 -0
- sql_assistant/llm/prompts.py +29 -0
- sql_assistant/llm/providers/__init__.py +1 -0
- sql_assistant/llm/providers/claude.py +132 -0
- sql_assistant/llm/providers/gemini.py +127 -0
- sql_assistant/llm/providers/openai_compatible.py +103 -0
- sql_assistant/llm/retry.py +88 -0
- sql_assistant/main.py +94 -0
- sql_assistant/settings.py +219 -0
- sql_assistant/web/__init__.py +1 -0
- sql_assistant/web/static/css/base.css +25 -0
- sql_assistant/web/static/css/components/backup.css +146 -0
- sql_assistant/web/static/css/components/chat.css +465 -0
- sql_assistant/web/static/css/components/modal.css +143 -0
- sql_assistant/web/static/css/components/settings.css +358 -0
- sql_assistant/web/static/css/components/sidebar.css +235 -0
- sql_assistant/web/static/css/components/toast.css +30 -0
- sql_assistant/web/static/css/style.css +10 -0
- sql_assistant/web/static/css/theme.css +200 -0
- sql_assistant/web/static/js/api.js +38 -0
- sql_assistant/web/static/js/app.js +161 -0
- sql_assistant/web/static/js/backup.js +216 -0
- sql_assistant/web/static/js/chat.js +238 -0
- sql_assistant/web/static/js/color-theme-manager.js +121 -0
- sql_assistant/web/static/js/confirm.js +95 -0
- sql_assistant/web/static/js/conversations.js +182 -0
- sql_assistant/web/static/js/settings.js +425 -0
- sql_assistant/web/static/js/state.js +43 -0
- sql_assistant/web/static/js/theme-manager.js +64 -0
- sql_assistant/web/static/js/ui.js +53 -0
- sql_assistant/web/templates/index.html +373 -0
- sql_assistant-1.0.0.dist-info/METADATA +24 -0
- sql_assistant-1.0.0.dist-info/RECORD +64 -0
- sql_assistant-1.0.0.dist-info/WHEEL +4 -0
- sql_assistant-1.0.0.dist-info/entry_points.txt +2 -0
- sql_assistant-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""API 路由模块"""
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"""备份相关 API 路由"""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
|
|
5
|
+
from .dependencies import get_config, get_db
|
|
6
|
+
from .models import (
|
|
7
|
+
BackupRequest, BackupResponse, BackupInfoResponse, BackupListResponse,
|
|
8
|
+
RestoreRequest, RestoreResponse,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.post("/backup", response_model=BackupResponse)
|
|
15
|
+
async def create_backup(request: BackupRequest):
|
|
16
|
+
from ..database.backup import get_backup_manager, BackupConfig
|
|
17
|
+
|
|
18
|
+
backup_manager = get_backup_manager()
|
|
19
|
+
|
|
20
|
+
config = BackupConfig(
|
|
21
|
+
backup_type=request.backup_type,
|
|
22
|
+
tables=request.tables,
|
|
23
|
+
include_schema=request.include_schema,
|
|
24
|
+
include_data=request.include_data
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
result = await backup_manager.backup(config)
|
|
28
|
+
|
|
29
|
+
return BackupResponse(
|
|
30
|
+
success=result.success,
|
|
31
|
+
message=result.message,
|
|
32
|
+
backup_id=result.backup_id,
|
|
33
|
+
backup_path=result.backup_path,
|
|
34
|
+
tables_backed_up=result.tables_backed_up,
|
|
35
|
+
total_records=result.total_records,
|
|
36
|
+
backup_size=result.backup_size,
|
|
37
|
+
backup_time=result.backup_time
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@router.get("/backup/list", response_model=BackupListResponse)
|
|
42
|
+
async def list_backups():
|
|
43
|
+
from ..database.backup import get_backup_manager
|
|
44
|
+
|
|
45
|
+
backup_manager = get_backup_manager()
|
|
46
|
+
backups = backup_manager.list_backups()
|
|
47
|
+
|
|
48
|
+
return BackupListResponse(
|
|
49
|
+
backups=[BackupInfoResponse(
|
|
50
|
+
backup_id=b.backup_id,
|
|
51
|
+
backup_type=b.backup_type,
|
|
52
|
+
db_type=b.db_type,
|
|
53
|
+
db_name=b.db_name,
|
|
54
|
+
tables=b.tables,
|
|
55
|
+
record_count=b.record_count,
|
|
56
|
+
backup_time=b.backup_time,
|
|
57
|
+
file_size=b.file_size
|
|
58
|
+
) for b in backups],
|
|
59
|
+
total=len(backups)
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@router.get("/backup/{backup_id}", response_model=BackupInfoResponse)
|
|
64
|
+
async def get_backup_info(backup_id: str):
|
|
65
|
+
from ..database.backup import get_backup_manager
|
|
66
|
+
|
|
67
|
+
backup_manager = get_backup_manager()
|
|
68
|
+
backup_info = backup_manager.get_backup_info(backup_id)
|
|
69
|
+
|
|
70
|
+
if not backup_info:
|
|
71
|
+
raise HTTPException(status_code=404, detail="备份不存在")
|
|
72
|
+
|
|
73
|
+
return BackupInfoResponse(
|
|
74
|
+
backup_id=backup_info.backup_id,
|
|
75
|
+
backup_type=backup_info.backup_type,
|
|
76
|
+
db_type=backup_info.db_type,
|
|
77
|
+
db_name=backup_info.db_name,
|
|
78
|
+
tables=backup_info.tables,
|
|
79
|
+
record_count=backup_info.record_count,
|
|
80
|
+
backup_time=backup_info.backup_time,
|
|
81
|
+
file_size=backup_info.file_size
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@router.delete("/backup/{backup_id}")
|
|
86
|
+
async def delete_backup(backup_id: str):
|
|
87
|
+
from ..database.backup import get_backup_manager
|
|
88
|
+
|
|
89
|
+
backup_manager = get_backup_manager()
|
|
90
|
+
|
|
91
|
+
if not backup_manager.delete_backup(backup_id):
|
|
92
|
+
raise HTTPException(status_code=404, detail="备份不存在")
|
|
93
|
+
|
|
94
|
+
return {"ok": True, "message": "备份删除成功"}
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@router.post("/backup/restore", response_model=RestoreResponse)
|
|
98
|
+
async def restore_backup(request: RestoreRequest):
|
|
99
|
+
from ..database.backup import get_backup_manager
|
|
100
|
+
|
|
101
|
+
backup_manager = get_backup_manager()
|
|
102
|
+
|
|
103
|
+
result = await backup_manager.restore(
|
|
104
|
+
backup_id=request.backup_id,
|
|
105
|
+
restore_schema=request.restore_schema,
|
|
106
|
+
restore_data=request.restore_data,
|
|
107
|
+
tables=request.tables
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return RestoreResponse(
|
|
111
|
+
success=result.success,
|
|
112
|
+
message=result.message,
|
|
113
|
+
backup_id=result.backup_id,
|
|
114
|
+
tables_restored=result.tables_restored,
|
|
115
|
+
total_records=result.total_records
|
|
116
|
+
)
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
"""配置相关 API 路由"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter, HTTPException
|
|
6
|
+
|
|
7
|
+
from ..settings import LLMProviderConfig, DatabaseConfig
|
|
8
|
+
|
|
9
|
+
from .dependencies import get_config, get_llm, get_db
|
|
10
|
+
from .models import (
|
|
11
|
+
LLMConfigRequest, LLMConfigResponse,
|
|
12
|
+
DatabaseConfigRequest, DatabaseConfigResponse,
|
|
13
|
+
TestConnectionRequest, TestConnectionResponse, TestLLMConnectionRequest,
|
|
14
|
+
AppSettingsResponse,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
router = APIRouter()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def _mask_api_key(key: str) -> str:
|
|
21
|
+
if len(key) <= 4:
|
|
22
|
+
return "****"
|
|
23
|
+
return "*" * (len(key) - 4) + key[-4:]
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@router.get("/config/llm/models")
|
|
27
|
+
async def get_llm_models(provider: Optional[str] = None):
|
|
28
|
+
from ..settings import PROVIDER_MODELS
|
|
29
|
+
|
|
30
|
+
if provider:
|
|
31
|
+
models = PROVIDER_MODELS.get(provider, [])
|
|
32
|
+
return {"provider": provider, "models": models}
|
|
33
|
+
|
|
34
|
+
return {"models": PROVIDER_MODELS}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("/config/llm", response_model=list[LLMConfigResponse])
|
|
38
|
+
async def list_llm_configs():
|
|
39
|
+
config = get_config()
|
|
40
|
+
return [
|
|
41
|
+
LLMConfigResponse(
|
|
42
|
+
name=p.name, provider=p.provider, model=p.get_model(),
|
|
43
|
+
enabled=p.enabled, api_key_masked=_mask_api_key(p.api_key),
|
|
44
|
+
)
|
|
45
|
+
for p in config.get_llm_providers()
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@router.post("/config/llm")
|
|
50
|
+
async def save_llm_config(req: LLMConfigRequest):
|
|
51
|
+
config = get_config()
|
|
52
|
+
llm_config = LLMProviderConfig(**req.model_dump())
|
|
53
|
+
config.add_llm_provider(llm_config)
|
|
54
|
+
return {"ok": True}
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@router.delete("/config/llm/{name}")
|
|
58
|
+
async def delete_llm_config(name: str):
|
|
59
|
+
config = get_config()
|
|
60
|
+
if not config.remove_llm_provider(name):
|
|
61
|
+
raise HTTPException(status_code=404, detail="LLM 配置不存在")
|
|
62
|
+
return {"ok": True}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@router.put("/config/llm/active/{name}")
|
|
66
|
+
async def set_active_llm(name: str):
|
|
67
|
+
config = get_config()
|
|
68
|
+
if not config.set_active_llm(name):
|
|
69
|
+
raise HTTPException(status_code=404, detail="LLM 配置不存在")
|
|
70
|
+
return {"ok": True}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@router.post("/config/llm/test", response_model=TestConnectionResponse)
|
|
74
|
+
async def test_llm_connection(req: TestLLMConnectionRequest):
|
|
75
|
+
llm = get_llm()
|
|
76
|
+
config_mgr = get_config()
|
|
77
|
+
|
|
78
|
+
if req.config:
|
|
79
|
+
llm_config = LLMProviderConfig(**req.config.model_dump())
|
|
80
|
+
from ..llm.manager import LLMManager
|
|
81
|
+
llm_manager = LLMManager()
|
|
82
|
+
provider = llm_manager.get_provider(llm_config)
|
|
83
|
+
result = await provider.test_connection()
|
|
84
|
+
elif req.name:
|
|
85
|
+
llm_config = config_mgr.get_llm_provider(req.name)
|
|
86
|
+
if not llm_config:
|
|
87
|
+
raise HTTPException(status_code=404, detail="LLM 配置不存在")
|
|
88
|
+
from ..llm.manager import LLMManager
|
|
89
|
+
llm_manager = LLMManager()
|
|
90
|
+
provider = llm_manager.get_provider(llm_config)
|
|
91
|
+
result = await provider.test_connection()
|
|
92
|
+
else:
|
|
93
|
+
result = {"success": False, "error": "请提供 LLM 配置名称或完整配置"}
|
|
94
|
+
|
|
95
|
+
return TestConnectionResponse(
|
|
96
|
+
success=result.get("success", False),
|
|
97
|
+
message=result.get("message") or (result.get("data") or {}).get("message") or result.get("error") or "未知错误",
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.get("/config/database", response_model=list[DatabaseConfigResponse])
|
|
102
|
+
async def list_database_configs():
|
|
103
|
+
config = get_config()
|
|
104
|
+
return [
|
|
105
|
+
DatabaseConfigResponse(
|
|
106
|
+
name=db.name, db_type=db.db_type, host=db.host,
|
|
107
|
+
port=db.get_port(), user=db.user,
|
|
108
|
+
database=db.database, enabled=db.enabled,
|
|
109
|
+
)
|
|
110
|
+
for db in config.get_databases()
|
|
111
|
+
]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@router.post("/config/database")
|
|
115
|
+
async def save_database_config(req: DatabaseConfigRequest):
|
|
116
|
+
config = get_config()
|
|
117
|
+
db_config = DatabaseConfig(**req.model_dump())
|
|
118
|
+
config.add_database(db_config)
|
|
119
|
+
return {"ok": True}
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@router.delete("/config/database/{name}")
|
|
123
|
+
async def delete_database_config(name: str):
|
|
124
|
+
config = get_config()
|
|
125
|
+
if not config.remove_database(name):
|
|
126
|
+
raise HTTPException(status_code=404, detail="数据库配置不存在")
|
|
127
|
+
return {"ok": True}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
@router.put("/config/database/active/{name}")
|
|
131
|
+
async def set_active_database(name: str):
|
|
132
|
+
config = get_config()
|
|
133
|
+
if not config.set_active_database(name):
|
|
134
|
+
raise HTTPException(status_code=404, detail="数据库配置不存在")
|
|
135
|
+
return {"ok": True}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
@router.post("/config/database/test", response_model=TestConnectionResponse)
|
|
139
|
+
async def test_database_connection(req: TestConnectionRequest):
|
|
140
|
+
db = get_db()
|
|
141
|
+
config_mgr = get_config()
|
|
142
|
+
|
|
143
|
+
if req.config:
|
|
144
|
+
db_config = DatabaseConfig(**req.config.model_dump())
|
|
145
|
+
result = await db.test_connection(db_config)
|
|
146
|
+
elif req.name:
|
|
147
|
+
db_config = config_mgr.get_database(req.name)
|
|
148
|
+
if not db_config:
|
|
149
|
+
raise HTTPException(status_code=404, detail="数据库配置不存在")
|
|
150
|
+
result = await db.test_connection(db_config)
|
|
151
|
+
else:
|
|
152
|
+
result = {"success": False, "error": "请提供数据库名称或完整配置"}
|
|
153
|
+
|
|
154
|
+
return TestConnectionResponse(
|
|
155
|
+
success=result.get("success", False),
|
|
156
|
+
message=result.get("message") or (result.get("data") or {}).get("message") or result.get("error") or "未知错误",
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
@router.get("/config/settings", response_model=AppSettingsResponse)
|
|
161
|
+
async def get_settings():
|
|
162
|
+
config = get_config()
|
|
163
|
+
settings = config.get_settings()
|
|
164
|
+
return AppSettingsResponse(
|
|
165
|
+
active_llm=settings.active_llm,
|
|
166
|
+
active_database=settings.active_database,
|
|
167
|
+
max_history_rows=settings.max_history_rows,
|
|
168
|
+
llm_providers=[
|
|
169
|
+
LLMConfigResponse(
|
|
170
|
+
name=p.name, provider=p.provider, model=p.get_model(),
|
|
171
|
+
enabled=p.enabled, api_key_masked=_mask_api_key(p.api_key),
|
|
172
|
+
)
|
|
173
|
+
for p in settings.llm_providers
|
|
174
|
+
],
|
|
175
|
+
databases=[
|
|
176
|
+
DatabaseConfigResponse(
|
|
177
|
+
name=db.name, db_type=db.db_type, host=db.host,
|
|
178
|
+
port=db.get_port(), user=db.user,
|
|
179
|
+
database=db.database, enabled=db.enabled,
|
|
180
|
+
)
|
|
181
|
+
for db in settings.databases
|
|
182
|
+
],
|
|
183
|
+
)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"""对话相关 API 路由"""
|
|
2
|
+
|
|
3
|
+
from fastapi import APIRouter, HTTPException
|
|
4
|
+
|
|
5
|
+
from .dependencies import get_history
|
|
6
|
+
from .models import (
|
|
7
|
+
ConversationRequest, ConversationResponse, ConversationDetailResponse,
|
|
8
|
+
ConversationListResponse, ConversationUpdateRequest, HistoryRecord,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
router = APIRouter()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@router.post("/conversations", response_model=ConversationResponse, status_code=201)
|
|
15
|
+
async def create_conversation(req: ConversationRequest):
|
|
16
|
+
history = get_history()
|
|
17
|
+
conversation_id = await history.create_conversation(title=req.title)
|
|
18
|
+
conversation = await history.get_conversation(conversation_id)
|
|
19
|
+
if not conversation:
|
|
20
|
+
raise HTTPException(status_code=500, detail="创建对话失败")
|
|
21
|
+
return ConversationResponse(**conversation)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@router.get("/conversations", response_model=ConversationListResponse)
|
|
25
|
+
async def list_conversations(limit: int = 50, offset: int = 0):
|
|
26
|
+
history = get_history()
|
|
27
|
+
conversations = await history.get_conversations(limit=limit, offset=offset)
|
|
28
|
+
total = await history.get_conversation_count()
|
|
29
|
+
return ConversationListResponse(
|
|
30
|
+
conversations=[ConversationResponse(**c) for c in conversations],
|
|
31
|
+
total=total,
|
|
32
|
+
limit=limit,
|
|
33
|
+
offset=offset,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
@router.get("/conversations/{conversation_id}", response_model=ConversationDetailResponse)
|
|
38
|
+
async def get_conversation(conversation_id: int):
|
|
39
|
+
history = get_history()
|
|
40
|
+
conversation = await history.get_conversation(conversation_id)
|
|
41
|
+
if not conversation:
|
|
42
|
+
raise HTTPException(status_code=404, detail="对话不存在")
|
|
43
|
+
|
|
44
|
+
messages = await history.get_conversation_messages(conversation_id)
|
|
45
|
+
return ConversationDetailResponse(
|
|
46
|
+
id=conversation["id"],
|
|
47
|
+
title=conversation["title"],
|
|
48
|
+
created_at=conversation.get("created_at", ""),
|
|
49
|
+
updated_at=conversation.get("updated_at", ""),
|
|
50
|
+
messages=[HistoryRecord(**m) for m in messages],
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@router.put("/conversations/{conversation_id}", response_model=ConversationResponse)
|
|
55
|
+
async def update_conversation(conversation_id: int, req: ConversationUpdateRequest):
|
|
56
|
+
history = get_history()
|
|
57
|
+
if not await history.update_conversation(conversation_id, req.title):
|
|
58
|
+
raise HTTPException(status_code=404, detail="对话不存在")
|
|
59
|
+
|
|
60
|
+
conversation = await history.get_conversation(conversation_id)
|
|
61
|
+
if not conversation:
|
|
62
|
+
raise HTTPException(status_code=500, detail="获取对话失败")
|
|
63
|
+
return ConversationResponse(**conversation)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@router.delete("/conversations/{conversation_id}")
|
|
67
|
+
async def delete_conversation(conversation_id: int):
|
|
68
|
+
history = get_history()
|
|
69
|
+
if not await history.delete_conversation(conversation_id):
|
|
70
|
+
raise HTTPException(status_code=404, detail="对话不存在")
|
|
71
|
+
return {"ok": True}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""FastAPI 依赖注入"""
|
|
2
|
+
|
|
3
|
+
from ..config import get_config_manager
|
|
4
|
+
from ..llm.manager import get_llm_manager
|
|
5
|
+
from ..database.manager import get_db_manager
|
|
6
|
+
from ..database.history import get_history_manager
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def get_config():
|
|
10
|
+
return get_config_manager()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def get_llm():
|
|
14
|
+
return get_llm_manager()
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def get_db():
|
|
18
|
+
return get_db_manager()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def get_history():
|
|
22
|
+
return get_history_manager()
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"""历史记录相关 API 路由"""
|
|
2
|
+
|
|
3
|
+
from typing import Optional
|
|
4
|
+
|
|
5
|
+
from fastapi import APIRouter
|
|
6
|
+
|
|
7
|
+
from .dependencies import get_history
|
|
8
|
+
from .models import HistoryRecord, HistoryListResponse
|
|
9
|
+
|
|
10
|
+
router = APIRouter()
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@router.get("/history", response_model=HistoryListResponse)
|
|
14
|
+
async def list_history(limit: int = 50, offset: int = 0, conversation_id: Optional[int] = None):
|
|
15
|
+
history = get_history()
|
|
16
|
+
records = await history.get_records(limit=limit, offset=offset, conversation_id=conversation_id)
|
|
17
|
+
total = await history.get_count(conversation_id=conversation_id)
|
|
18
|
+
return HistoryListResponse(
|
|
19
|
+
records=[
|
|
20
|
+
HistoryRecord(
|
|
21
|
+
id=r["id"],
|
|
22
|
+
conversation_id=r.get("conversation_id"),
|
|
23
|
+
question=r["question"],
|
|
24
|
+
sql=r["sql"],
|
|
25
|
+
result_json=r.get("result_json"),
|
|
26
|
+
db_type=r.get("db_type", ""),
|
|
27
|
+
llm_provider=r.get("llm_provider", ""),
|
|
28
|
+
success=bool(r.get("success", 1)),
|
|
29
|
+
error_message=r.get("error_message", ""),
|
|
30
|
+
created_at=r.get("created_at", ""),
|
|
31
|
+
)
|
|
32
|
+
for r in records
|
|
33
|
+
],
|
|
34
|
+
total=total,
|
|
35
|
+
limit=limit,
|
|
36
|
+
offset=offset,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@router.delete("/history/{record_id}")
|
|
41
|
+
async def delete_history_record(record_id: int):
|
|
42
|
+
history = get_history()
|
|
43
|
+
if not await history.delete_record(record_id):
|
|
44
|
+
return {"ok": False, "error": "记录不存在"}
|
|
45
|
+
return {"ok": True}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@router.delete("/history")
|
|
49
|
+
async def clear_history():
|
|
50
|
+
history = get_history()
|
|
51
|
+
count = await history.clear_history()
|
|
52
|
+
return {"ok": True, "deleted": count}
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@router.get("/history/{record_id}")
|
|
56
|
+
async def get_history_record(record_id: int):
|
|
57
|
+
history = get_history()
|
|
58
|
+
record = await history.get_record(record_id)
|
|
59
|
+
if not record:
|
|
60
|
+
return {"error": "记录不存在"}
|
|
61
|
+
return record
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""API 数据模型"""
|
|
2
|
+
|
|
3
|
+
from pydantic import BaseModel, Field
|
|
4
|
+
from typing import Optional, Any, List
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ---- Query ----
|
|
8
|
+
|
|
9
|
+
class QueryRequest(BaseModel):
|
|
10
|
+
question: str = Field(description="自然语言查询问题")
|
|
11
|
+
db_type_override: Optional[str] = Field(default=None, description="临时切换数据库类型")
|
|
12
|
+
conversation_id: Optional[int] = Field(default=None, description="对话ID,用于在已有对话中添加消息")
|
|
13
|
+
page: int = Field(default=1, ge=1, description="结果页码")
|
|
14
|
+
page_size: int = Field(default=100, ge=1, le=1000, description="每页记录数")
|
|
15
|
+
confirmed: bool = Field(default=False, description="用户是否已确认执行")
|
|
16
|
+
sql_hash: Optional[str] = Field(default=None, description="要确认执行的 SQL 哈希,用于验证")
|
|
17
|
+
sql: Optional[str] = Field(default=None, description="用户确认后要执行的 SQL 语句(confirmed=True 时使用)")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class SQLPreviewResponse(BaseModel):
|
|
21
|
+
"""SQL 预览响应"""
|
|
22
|
+
success: bool
|
|
23
|
+
sql: str = ""
|
|
24
|
+
sql_hash: str = ""
|
|
25
|
+
requires_confirmation: bool = False
|
|
26
|
+
confirmation_reason: str = ""
|
|
27
|
+
warning: str = ""
|
|
28
|
+
risk_level: str = "none"
|
|
29
|
+
error: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class QueryResponse(BaseModel):
|
|
33
|
+
success: bool
|
|
34
|
+
question: str = ""
|
|
35
|
+
sql: str = ""
|
|
36
|
+
result: Optional[dict] = None
|
|
37
|
+
error: Optional[str] = None
|
|
38
|
+
history_id: Optional[int] = None
|
|
39
|
+
conversation_id: Optional[int] = None
|
|
40
|
+
pagination: Optional[dict] = None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ---- Conversation ----
|
|
44
|
+
|
|
45
|
+
class ConversationRequest(BaseModel):
|
|
46
|
+
title: Optional[str] = Field(default="", description="对话标题")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ConversationUpdateRequest(BaseModel):
|
|
50
|
+
title: str = Field(description="新的对话标题")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class ConversationResponse(BaseModel):
|
|
54
|
+
id: int
|
|
55
|
+
title: str
|
|
56
|
+
created_at: str = ""
|
|
57
|
+
updated_at: str = ""
|
|
58
|
+
message_count: int = 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class ConversationDetailResponse(BaseModel):
|
|
62
|
+
id: int
|
|
63
|
+
title: str
|
|
64
|
+
created_at: str = ""
|
|
65
|
+
updated_at: str = ""
|
|
66
|
+
messages: list["HistoryRecord"] = Field(default_factory=list)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class ConversationListResponse(BaseModel):
|
|
70
|
+
conversations: list[ConversationResponse]
|
|
71
|
+
total: int
|
|
72
|
+
limit: int
|
|
73
|
+
offset: int
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# ---- Config: LLM ----
|
|
77
|
+
|
|
78
|
+
class LLMConfigRequest(BaseModel):
|
|
79
|
+
name: str
|
|
80
|
+
provider: str # deepseek, doubao, kimi, qwen, gemini, claude, openai
|
|
81
|
+
api_key: str = ""
|
|
82
|
+
base_url: str = ""
|
|
83
|
+
model: str = ""
|
|
84
|
+
enabled: bool = True
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class LLMConfigResponse(BaseModel):
|
|
88
|
+
name: str
|
|
89
|
+
provider: str
|
|
90
|
+
model: str
|
|
91
|
+
enabled: bool
|
|
92
|
+
api_key_masked: str = "" # 只显示后4位
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
# ---- Config: Database ----
|
|
96
|
+
|
|
97
|
+
class DatabaseConfigRequest(BaseModel):
|
|
98
|
+
name: str
|
|
99
|
+
db_type: str # mysql, sqlserver, postgresql, redis, mongodb
|
|
100
|
+
host: str = "localhost"
|
|
101
|
+
port: int = 0
|
|
102
|
+
user: str = ""
|
|
103
|
+
password: str = ""
|
|
104
|
+
database: str = ""
|
|
105
|
+
enabled: bool = True
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class DatabaseConfigResponse(BaseModel):
|
|
109
|
+
name: str
|
|
110
|
+
db_type: str
|
|
111
|
+
host: str
|
|
112
|
+
port: int
|
|
113
|
+
user: str
|
|
114
|
+
database: str
|
|
115
|
+
enabled: bool
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class TestConnectionRequest(BaseModel):
|
|
119
|
+
"""测试连接请求 - 可传完整配置或只传名称"""
|
|
120
|
+
name: Optional[str] = None
|
|
121
|
+
config: Optional[DatabaseConfigRequest] = None
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class TestLLMConnectionRequest(BaseModel):
|
|
125
|
+
"""测试 LLM 连接请求"""
|
|
126
|
+
name: Optional[str] = None
|
|
127
|
+
config: Optional[LLMConfigRequest] = None
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class TestConnectionResponse(BaseModel):
|
|
131
|
+
success: bool
|
|
132
|
+
message: str
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# ---- Config: Settings ----
|
|
136
|
+
|
|
137
|
+
class AppSettingsResponse(BaseModel):
|
|
138
|
+
active_llm: str = ""
|
|
139
|
+
active_database: str = ""
|
|
140
|
+
max_history_rows: int = 1000
|
|
141
|
+
llm_providers: list[LLMConfigResponse] = Field(default_factory=list)
|
|
142
|
+
databases: list[DatabaseConfigResponse] = Field(default_factory=list)
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
# ---- History ----
|
|
146
|
+
|
|
147
|
+
class HistoryRecord(BaseModel):
|
|
148
|
+
id: int
|
|
149
|
+
conversation_id: Optional[int] = None
|
|
150
|
+
question: str
|
|
151
|
+
sql: str
|
|
152
|
+
result_json: Optional[str] = None
|
|
153
|
+
db_type: str = ""
|
|
154
|
+
llm_provider: str = ""
|
|
155
|
+
success: bool = True
|
|
156
|
+
error_message: str = ""
|
|
157
|
+
created_at: str = ""
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class HistoryListResponse(BaseModel):
|
|
161
|
+
records: list[HistoryRecord]
|
|
162
|
+
total: int
|
|
163
|
+
limit: int
|
|
164
|
+
offset: int
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ---- Backup ----
|
|
168
|
+
|
|
169
|
+
class BackupRequest(BaseModel):
|
|
170
|
+
"""备份请求模型"""
|
|
171
|
+
backup_type: str = Field(default="full", description="备份类型: full(全量备份) / incremental(增量备份)")
|
|
172
|
+
tables: Optional[List[str]] = Field(default=None, description="要备份的表名列表,为空则备份所有表")
|
|
173
|
+
include_schema: bool = Field(default=True, description="是否包含表结构")
|
|
174
|
+
include_data: bool = Field(default=True, description="是否包含数据")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class BackupResponse(BaseModel):
|
|
178
|
+
"""备份响应模型"""
|
|
179
|
+
success: bool
|
|
180
|
+
message: str
|
|
181
|
+
backup_id: str = ""
|
|
182
|
+
backup_path: str = ""
|
|
183
|
+
tables_backed_up: List[str] = Field(default_factory=list)
|
|
184
|
+
total_records: int = 0
|
|
185
|
+
backup_size: int = 0 # bytes
|
|
186
|
+
backup_time: str = ""
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
class BackupInfoResponse(BaseModel):
|
|
190
|
+
"""备份信息响应模型"""
|
|
191
|
+
backup_id: str
|
|
192
|
+
backup_type: str
|
|
193
|
+
db_type: str = ""
|
|
194
|
+
db_name: str = ""
|
|
195
|
+
tables: List[str] = Field(default_factory=list)
|
|
196
|
+
record_count: int = 0
|
|
197
|
+
backup_time: str = ""
|
|
198
|
+
file_size: int = 0 # bytes
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
class BackupListResponse(BaseModel):
|
|
202
|
+
"""备份列表响应模型"""
|
|
203
|
+
backups: List[BackupInfoResponse]
|
|
204
|
+
total: int
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
class RestoreRequest(BaseModel):
|
|
208
|
+
"""恢复备份请求模型"""
|
|
209
|
+
backup_id: str = Field(description="要恢复的备份ID")
|
|
210
|
+
restore_schema: bool = Field(default=False, description="是否重建表结构(谨慎使用,会删除现有表)")
|
|
211
|
+
restore_data: bool = Field(default=True, description="是否恢复数据")
|
|
212
|
+
tables: Optional[List[str]] = Field(default=None, description="指定要恢复的表,为空则恢复所有表")
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
class RestoreResponse(BaseModel):
|
|
216
|
+
"""恢复备份响应模型"""
|
|
217
|
+
success: bool
|
|
218
|
+
message: str
|
|
219
|
+
backup_id: str = ""
|
|
220
|
+
tables_restored: List[str] = Field(default_factory=list)
|
|
221
|
+
total_records: int = 0
|