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.
Files changed (64) hide show
  1. sql_assistant/__init__.py +3 -0
  2. sql_assistant/api/__init__.py +1 -0
  3. sql_assistant/api/backup.py +116 -0
  4. sql_assistant/api/config.py +183 -0
  5. sql_assistant/api/conversation.py +71 -0
  6. sql_assistant/api/dependencies.py +22 -0
  7. sql_assistant/api/history.py +61 -0
  8. sql_assistant/api/models.py +221 -0
  9. sql_assistant/api/query.py +275 -0
  10. sql_assistant/api/routes.py +19 -0
  11. sql_assistant/api/schema.py +21 -0
  12. sql_assistant/config.py +144 -0
  13. sql_assistant/database/__init__.py +1 -0
  14. sql_assistant/database/backup.py +568 -0
  15. sql_assistant/database/connectors/__init__.py +1 -0
  16. sql_assistant/database/connectors/base.py +185 -0
  17. sql_assistant/database/connectors/exceptions.py +88 -0
  18. sql_assistant/database/connectors/mongodb.py +194 -0
  19. sql_assistant/database/connectors/mysql.py +110 -0
  20. sql_assistant/database/connectors/postgresql.py +133 -0
  21. sql_assistant/database/connectors/redis.py +132 -0
  22. sql_assistant/database/connectors/sqlserver.py +140 -0
  23. sql_assistant/database/history.py +290 -0
  24. sql_assistant/database/manager.py +178 -0
  25. sql_assistant/database/security.py +230 -0
  26. sql_assistant/llm/__init__.py +1 -0
  27. sql_assistant/llm/base.py +28 -0
  28. sql_assistant/llm/exceptions.py +96 -0
  29. sql_assistant/llm/manager.py +82 -0
  30. sql_assistant/llm/prompts.py +29 -0
  31. sql_assistant/llm/providers/__init__.py +1 -0
  32. sql_assistant/llm/providers/claude.py +132 -0
  33. sql_assistant/llm/providers/gemini.py +127 -0
  34. sql_assistant/llm/providers/openai_compatible.py +103 -0
  35. sql_assistant/llm/retry.py +88 -0
  36. sql_assistant/main.py +94 -0
  37. sql_assistant/settings.py +219 -0
  38. sql_assistant/web/__init__.py +1 -0
  39. sql_assistant/web/static/css/base.css +25 -0
  40. sql_assistant/web/static/css/components/backup.css +146 -0
  41. sql_assistant/web/static/css/components/chat.css +465 -0
  42. sql_assistant/web/static/css/components/modal.css +143 -0
  43. sql_assistant/web/static/css/components/settings.css +358 -0
  44. sql_assistant/web/static/css/components/sidebar.css +235 -0
  45. sql_assistant/web/static/css/components/toast.css +30 -0
  46. sql_assistant/web/static/css/style.css +10 -0
  47. sql_assistant/web/static/css/theme.css +200 -0
  48. sql_assistant/web/static/js/api.js +38 -0
  49. sql_assistant/web/static/js/app.js +161 -0
  50. sql_assistant/web/static/js/backup.js +216 -0
  51. sql_assistant/web/static/js/chat.js +238 -0
  52. sql_assistant/web/static/js/color-theme-manager.js +121 -0
  53. sql_assistant/web/static/js/confirm.js +95 -0
  54. sql_assistant/web/static/js/conversations.js +182 -0
  55. sql_assistant/web/static/js/settings.js +425 -0
  56. sql_assistant/web/static/js/state.js +43 -0
  57. sql_assistant/web/static/js/theme-manager.js +64 -0
  58. sql_assistant/web/static/js/ui.js +53 -0
  59. sql_assistant/web/templates/index.html +373 -0
  60. sql_assistant-1.0.0.dist-info/METADATA +24 -0
  61. sql_assistant-1.0.0.dist-info/RECORD +64 -0
  62. sql_assistant-1.0.0.dist-info/WHEEL +4 -0
  63. sql_assistant-1.0.0.dist-info/entry_points.txt +2 -0
  64. sql_assistant-1.0.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,3 @@
1
+ """SQL 智能助手 - 自然语言转 SQL 查询工具"""
2
+
3
+ __version__ = "1.0.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