Skip to content

Commit 79f7630

Browse files
perf: Encryption of sensitive data in ai_model table #9
1 parent 5b5d504 commit 79f7630

File tree

10 files changed

+130
-11
lines changed

10 files changed

+130
-11
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
"""040_modify_ai_model
2+
3+
Revision ID: 0fc14c2cfe41
4+
Revises: 25cbc85766fd
5+
Create Date: 2025-08-26 23:30:50.192799
6+
7+
"""
8+
from alembic import op
9+
import sqlalchemy as sa
10+
import sqlmodel.sql.sqltypes
11+
12+
13+
# revision identifiers, used by Alembic.
14+
revision = '0fc14c2cfe41'
15+
down_revision = '25cbc85766fd'
16+
branch_labels = None
17+
depends_on = None
18+
19+
20+
def upgrade():
21+
op.alter_column(
22+
'ai_model',
23+
'api_key',
24+
type_=sa.Text(),
25+
existing_type=sa.String(length=255),
26+
existing_nullable=True
27+
)
28+
op.alter_column(
29+
'ai_model',
30+
'api_domain',
31+
type_=sa.Text(),
32+
existing_type=sa.String(length=255),
33+
existing_nullable=False
34+
)
35+
36+
37+
def downgrade():
38+
op.alter_column(
39+
'ai_model',
40+
'api_key',
41+
type_=sa.String(),
42+
existing_type=sa.Text(),
43+
existing_nullable=True
44+
)
45+
op.alter_column(
46+
'ai_model',
47+
'api_domain',
48+
type_=sa.String(),
49+
existing_type=sa.Text(),
50+
existing_nullable=False
51+
)

backend/apps/ai_model/model_factory.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from apps.ai_model.openai.llm import BaseChatOpenAI
1111
from apps.system.models.system_model import AiModelDetail
1212
from common.core.db import engine
13+
from common.utils.crypto import sqlbot_decrypt
1314
from common.utils.utils import prepare_model_arg
1415
from langchain_community.llms import VLLMOpenAI
1516
from langchain_openai import AzureChatOpenAI
@@ -137,7 +138,7 @@ def register_llm(cls, model_type: str, llm_class: Type[BaseLLM]):
137138
return config """
138139

139140

140-
def get_default_config() -> LLMConfig:
141+
async def get_default_config() -> LLMConfig:
141142
with Session(engine) as session:
142143
db_model = session.exec(
143144
select(AiModelDetail).where(AiModelDetail.default_model == True)
@@ -152,6 +153,11 @@ def get_default_config() -> LLMConfig:
152153
additional_params = {item["key"]: prepare_model_arg(item.get('val')) for item in config_raw if "key" in item and "val" in item}
153154
except Exception:
154155
pass
156+
if not db_model.api_domain.startswith("http"):
157+
db_model.api_domain = await sqlbot_decrypt(db_model.api_domain)
158+
if db_model.api_key:
159+
db_model.api_key = await sqlbot_decrypt(db_model.api_key)
160+
155161

156162
# 构造 LLMConfig
157163
return LLMConfig(

backend/apps/chat/api/chat.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ async def recommend_questions(session: SessionDep, current_user: CurrentUser, ch
114114
)
115115
request_question = ChatQuestion(chat_id=record.chat_id, question=record.question if record.question else '')
116116

117-
llm_service = LLMService(current_user, request_question, current_assistant, True)
117+
llm_service = await LLMService.create(current_user, request_question, current_assistant, True)
118118
llm_service.set_record(record)
119119
llm_service.run_recommend_questions_task_async()
120120
except Exception as e:
@@ -142,7 +142,7 @@ async def stream_sql(session: SessionDep, current_user: CurrentUser, request_que
142142
"""
143143

144144
try:
145-
llm_service = LLMService(current_user, request_question, current_assistant)
145+
llm_service = await LLMService.create(current_user, request_question, current_assistant)
146146
llm_service.init_record()
147147
llm_service.run_task_async()
148148
except Exception as e:
@@ -189,7 +189,7 @@ async def analysis_or_predict(session: SessionDep, current_user: CurrentUser, ch
189189
request_question = ChatQuestion(chat_id=record.chat_id, question=record.question)
190190

191191
try:
192-
llm_service = LLMService(current_user, request_question, current_assistant)
192+
llm_service = await LLMService.create(current_user, request_question, current_assistant)
193193
llm_service.run_analysis_or_predict_task_async(action_type, record)
194194
except Exception as e:
195195
traceback.print_exc()

backend/apps/chat/task/llm.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ class LLMService:
7070
future: Future
7171

7272
def __init__(self, current_user: CurrentUser, chat_question: ChatQuestion,
73-
current_assistant: Optional[CurrentAssistant] = None, no_reasoning: bool = False):
73+
current_assistant: Optional[CurrentAssistant] = None, no_reasoning: bool = False, config: LLMConfig = None):
7474
self.chunk_list = []
7575
engine = create_engine(str(settings.SQLALCHEMY_DATABASE_URI))
7676
session_maker = sessionmaker(bind=engine)
@@ -110,7 +110,7 @@ def __init__(self, current_user: CurrentUser, chat_question: ChatQuestion,
110110

111111
self.ds = (ds if isinstance(ds, AssistantOutDsSchema) else CoreDatasource(**ds.model_dump())) if ds else None
112112
self.chat_question = chat_question
113-
self.config = get_default_config()
113+
self.config = config
114114
if no_reasoning:
115115
# only work while using qwen
116116
if self.config.additional_params:
@@ -125,6 +125,14 @@ def __init__(self, current_user: CurrentUser, chat_question: ChatQuestion,
125125
llm_instance = LLMFactory.create_llm(self.config)
126126
self.llm = llm_instance.llm
127127

128+
self.init_messages()
129+
130+
@classmethod
131+
async def create(cls, *args, **kwargs):
132+
config: LLMConfig = await get_default_config()
133+
instance = cls(*args, **kwargs, config=config)
134+
return instance
135+
128136
def is_running(self, timeout=0.5):
129137
try:
130138
r = concurrent.futures.wait([self.future], timeout)

backend/apps/mcp/mcp.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ async def mcp_question(session: SessionDep, chat: McpQuestion):
107107

108108
mcp_chat = ChatMcp(token=chat.token, chat_id=chat.chat_id, question=chat.question)
109109
# ask
110-
llm_service = LLMService(session_user, mcp_chat)
110+
llm_service = await LLMService.create(session_user, mcp_chat)
111111
llm_service.init_record()
112112

113113
return StreamingResponse(llm_service.run_task(False), media_type="text/event-stream")

backend/apps/system/api/aimodel.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from apps.system.models.system_model import AiModelDetail
1111
from common.core.deps import SessionDep, Trans
12+
from common.utils.crypto import sqlbot_decrypt
1213
from common.utils.time import get_timestamp
1314
from common.utils.utils import SQLBotLogUtil, prepare_model_arg
1415

@@ -102,6 +103,10 @@ async def get_model_by_id(
102103
config_list = [AiModelConfigItem(**item) for item in raw]
103104
except Exception:
104105
pass
106+
if db_model.api_key:
107+
db_model.api_key = await sqlbot_decrypt(db_model.api_key)
108+
if db_model.api_domain:
109+
db_model.api_domain = await sqlbot_decrypt(db_model.api_domain)
105110
data = AiModelDetail.model_validate(db_model).model_dump(exclude_unset=True)
106111
data.pop("config", None)
107112
data["config_list"] = config_list
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
2+
from apps.system.models.system_model import AiModelDetail
3+
from common.core.db import engine
4+
from sqlmodel import Session, select
5+
from common.utils.crypto import sqlbot_encrypt
6+
from common.utils.utils import SQLBotLogUtil
7+
8+
async def async_model_info():
9+
with Session(engine) as session:
10+
model_list = session.exec(select(AiModelDetail)).all()
11+
any_model_change = False
12+
if model_list:
13+
for model in model_list:
14+
if model.api_domain.startswith("http"):
15+
if model.api_key:
16+
model.api_key = await sqlbot_encrypt(model.api_key)
17+
if model.api_domain:
18+
model.api_domain = await sqlbot_encrypt(model.api_domain)
19+
session.add(model)
20+
any_model_change = True
21+
if any_model_change:
22+
session.commit()
23+
SQLBotLogUtil.info("✅ 异步加密已有模型的密钥和地址完成")
24+
25+
26+

backend/common/utils/crypto.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
from sqlbot_xpack.core import sqlbot_decrypt as xpack_sqlbot_decrypt
1+
from sqlbot_xpack.core import sqlbot_decrypt as xpack_sqlbot_decrypt, sqlbot_encrypt as xpack_sqlbot_encrypt
22

33
async def sqlbot_decrypt(text: str) -> str:
4-
return await xpack_sqlbot_decrypt(text)
4+
return await xpack_sqlbot_decrypt(text)
5+
6+
async def sqlbot_encrypt(text: str) -> str:
7+
return await xpack_sqlbot_encrypt(text)

backend/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from alembic import command
1212
from apps.api import api_router
13+
from apps.system.crud.aimodel_manage import async_model_info
1314
from apps.system.crud.assistant import init_dynamic_cors
1415
from apps.system.middleware.auth import TokenMiddleware
1516
from apps.terminology.curd.terminology import fill_empty_embeddings
@@ -36,6 +37,7 @@ async def lifespan(app: FastAPI):
3637
init_embedding_data()
3738
SQLBotLogUtil.info("✅ SQLBot 初始化完成")
3839
await sqlbot_xpack.core.clean_xpack_cache()
40+
await async_model_info() # 异步加密已有模型的密钥和地址
3941
yield
4042
SQLBotLogUtil.info("SQLBot 应用关闭")
4143

frontend/src/api/system.ts

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,26 @@ import { request } from '@/utils/request'
33
export const modelApi = {
44
queryAll: (keyword?: string) =>
55
request.get('/system/aimodel', { params: keyword ? { keyword } : {} }),
6-
add: (data: any) => request.post('/system/aimodel', data),
7-
edit: (data: any) => request.put('/system/aimodel', data),
6+
add: (data: any) => {
7+
const param = data
8+
if (param.api_key) {
9+
param.api_key = LicenseGenerator.sqlbotEncrypt(data.api_key)
10+
}
11+
if (param.api_domain) {
12+
param.api_domain = LicenseGenerator.sqlbotEncrypt(data.api_domain)
13+
}
14+
return request.post('/system/aimodel', param)
15+
},
16+
edit: (data: any) => {
17+
const param = data
18+
if (param.api_key) {
19+
param.api_key = LicenseGenerator.sqlbotEncrypt(data.api_key)
20+
}
21+
if (param.api_domain) {
22+
param.api_domain = LicenseGenerator.sqlbotEncrypt(data.api_domain)
23+
}
24+
return request.put('/system/aimodel', param)
25+
},
826
delete: (id: number) => request.delete(`/system/aimodel/${id}`),
927
query: (id: number) => request.get(`/system/aimodel/${id}`),
1028
setDefault: (id: number) => request.put(`/system/aimodel/default/${id}`),

0 commit comments

Comments
 (0)