Skip to content

Commit c8d8abc

Browse files
niiish32xnishenghao.nsh
andauthored
feat: unify LLM settings (default model, multi-provider, models, keys) and fix oauth2 mysql persistenr bug (#181)
Co-authored-by: nishenghao.nsh <nishenghao.nsh@oceanbase.com>
1 parent f60bed8 commit c8d8abc

8 files changed

Lines changed: 1252 additions & 356 deletions

File tree

packages/derisk-app/src/derisk_app/app.py

Lines changed: 11 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -173,26 +173,24 @@ def _sync_oauth2_config_from_db():
173173
"""
174174
try:
175175
from derisk_app.config_storage.oauth2_db_storage import get_oauth2_db_storage
176-
from derisk.configs.model_config import Config
176+
from derisk_core.config import ConfigManager, OAuth2Config
177177

178178
db_storage = get_oauth2_db_storage()
179179
# Load with actual secrets for runtime use
180180
db_oauth2 = db_storage.load_with_secrets()
181181

182182
if db_oauth2 is not None:
183183
# Update the runtime config with database values
184-
cfg = Config()
185-
if hasattr(cfg, "oauth2"):
186-
from derisk_core.config import OAuth2Config
187-
188-
# Convert dict to OAuth2Config
189-
oauth2_config = OAuth2Config(
190-
enabled=db_oauth2.get("enabled", False),
191-
providers=db_oauth2.get("providers", []),
192-
admin_users=db_oauth2.get("admin_users", []),
193-
)
194-
cfg.oauth2 = oauth2_config
195-
logger.info("OAuth2 config loaded from database (secrets loaded for runtime)")
184+
cfg = ConfigManager.get()
185+
oauth2_config = OAuth2Config(
186+
enabled=db_oauth2.get("enabled", False),
187+
providers=db_oauth2.get("providers", []),
188+
admin_users=db_oauth2.get("admin_users", []),
189+
)
190+
cfg.oauth2 = oauth2_config
191+
logger.info(
192+
"OAuth2 config loaded from database (secrets loaded for runtime)"
193+
)
196194
else:
197195
logger.info("No OAuth2 config in database, using file config")
198196
except Exception as e:

packages/derisk-app/src/derisk_app/base.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,127 @@ def _initialize_db_storage(param: ServiceConfig, system_app: SystemApp):
8888
try_to_create_db=not disable_alembic_upgrade,
8989
system_app=system_app,
9090
)
91+
_migrate_mysql_chat_tables_utf8mb4(db_url)
92+
93+
94+
def _migrate_mysql_chat_tables_utf8mb4(db_url: str) -> None:
95+
"""Ensure chat history tables use utf8mb4 (fixes DataError 1366 on emoji).
96+
97+
New installs get correct DDL from assets/schema/derisk.sql. Legacy MySQL
98+
databases may still have utf8/utf8mb3 tables; migrate them automatically.
99+
"""
100+
if "mysql" not in (db_url or "").lower():
101+
return
102+
try:
103+
from sqlalchemy import text
104+
from derisk.storage.metadata.db_manager import db
105+
106+
if not db.is_initialized:
107+
return
108+
109+
engine = db.engine
110+
with engine.connect() as conn:
111+
tables = conn.execute(
112+
text(
113+
"""
114+
SELECT TABLE_NAME, TABLE_COLLATION
115+
FROM information_schema.TABLES
116+
WHERE TABLE_SCHEMA = DATABASE()
117+
AND TABLE_NAME IN ('chat_history', 'chat_history_message')
118+
"""
119+
)
120+
).fetchall()
121+
122+
if not tables:
123+
return
124+
125+
def _collation_ok(collation: object) -> bool:
126+
return str(collation or "").lower().startswith("utf8mb4")
127+
128+
bad_tables = [
129+
name for name, coll in tables if not _collation_ok(coll)
130+
]
131+
132+
msg_detail_charset = None
133+
row = conn.execute(
134+
text(
135+
"""
136+
SELECT CHARACTER_SET_NAME
137+
FROM information_schema.COLUMNS
138+
WHERE TABLE_SCHEMA = DATABASE()
139+
AND TABLE_NAME = 'chat_history_message'
140+
AND COLUMN_NAME = 'message_detail'
141+
"""
142+
)
143+
).fetchone()
144+
if row:
145+
msg_detail_charset = (row[0] or "").lower()
146+
147+
bad_column = msg_detail_charset and msg_detail_charset != "utf8mb4"
148+
149+
if not bad_tables and not bad_column:
150+
return
151+
152+
schema = conn.execute(text("SELECT DATABASE()")).scalar()
153+
if not schema:
154+
return
155+
156+
# DDL auto-commits; use begin() for a clean transaction boundary per statement.
157+
migrated = False
158+
with engine.begin() as conn:
159+
db_coll = conn.execute(
160+
text(
161+
"""
162+
SELECT DEFAULT_COLLATION_NAME
163+
FROM information_schema.SCHEMATA
164+
WHERE SCHEMA_NAME = :schema
165+
"""
166+
),
167+
{"schema": schema},
168+
).scalar()
169+
if db_coll and not str(db_coll).lower().startswith("utf8mb4"):
170+
conn.execute(
171+
text(
172+
"ALTER DATABASE `{schema}` CHARACTER SET utf8mb4 "
173+
"COLLATE utf8mb4_unicode_ci".format(schema=schema)
174+
)
175+
)
176+
migrated = True
177+
178+
if "chat_history" in bad_tables:
179+
conn.execute(
180+
text(
181+
"ALTER TABLE `chat_history` CONVERT TO CHARACTER SET utf8mb4 "
182+
"COLLATE utf8mb4_unicode_ci"
183+
)
184+
)
185+
migrated = True
186+
if "chat_history_message" in bad_tables:
187+
conn.execute(
188+
text(
189+
"ALTER TABLE `chat_history_message` "
190+
"CONVERT TO CHARACTER SET utf8mb4 "
191+
"COLLATE utf8mb4_unicode_ci"
192+
)
193+
)
194+
migrated = True
195+
elif bad_column:
196+
conn.execute(
197+
text(
198+
"ALTER TABLE `chat_history_message` "
199+
"MODIFY COLUMN `message_detail` LONGTEXT "
200+
"CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL "
201+
"COMMENT 'Message details, json format'"
202+
)
203+
)
204+
migrated = True
205+
206+
if migrated:
207+
logger.info(
208+
"MySQL chat_history / chat_history_message migrated to utf8mb4."
209+
)
210+
except Exception as e:
211+
logger.error("MySQL utf8mb4 migration for chat tables failed: %s", e)
91212

92213

93214
def _add_missing_columns_sqlite(db):

packages/derisk-app/src/derisk_app/config_storage/oauth2_db_storage.py

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,8 @@
88
import logging
99
from typing import Any, Dict, List, Optional
1010

11-
from cryptography.fernet import Fernet
1211
from sqlalchemy import Column, DateTime, Integer, String, Text
1312

14-
from derisk._private.config import Config
1513
from derisk.storage.metadata import BaseDao, Model
1614

1715
logger = logging.getLogger(__name__)
@@ -182,28 +180,37 @@ def _mask_providers_for_display(
182180
provider["client_secret"] = "****"
183181
return masked
184182

185-
def get_config(self, config_key: str = "global", mask_secrets: bool = True) -> Optional[Dict[str, Any]]:
183+
def get_config(
184+
self, config_key: str = "global", mask_secrets: bool = True
185+
) -> Optional[Dict[str, Any]]:
186186
"""Get OAuth2 config from database.
187187
188188
Args:
189189
config_key: Configuration key (default: global)
190190
mask_secrets: If True, mask client_secret in providers for display
191191
"""
192-
entity = self.get_by_key(config_key)
193-
if not entity:
194-
return None
192+
with self.session() as session:
193+
entity = (
194+
session.query(OAuth2ConfigEntity)
195+
.filter(OAuth2ConfigEntity.config_key == config_key)
196+
.first()
197+
)
198+
if not entity:
199+
return None
200+
201+
# Read ORM attributes before the session closes to avoid
202+
# detached-instance access during JSON conversion.
203+
enabled = bool(entity.enabled)
204+
admin_users_json = entity.admin_users_json or "[]"
205+
providers_json = entity.providers_json or "[]"
195206

196207
try:
197-
admin_users = (
198-
json.loads(entity.admin_users_json)
199-
if entity.admin_users_json
200-
else []
201-
)
208+
admin_users = json.loads(admin_users_json) if admin_users_json else []
202209
except json.JSONDecodeError:
203210
admin_users = []
204211

205212
try:
206-
providers = json.loads(entity.providers_json or "[]")
213+
providers = json.loads(providers_json)
207214
except json.JSONDecodeError:
208215
providers = []
209216

@@ -212,12 +219,14 @@ def get_config(self, config_key: str = "global", mask_secrets: bool = True) -> O
212219
providers = self._mask_providers_for_display(providers)
213220

214221
return {
215-
"enabled": bool(entity.enabled),
222+
"enabled": enabled,
216223
"providers": providers,
217224
"admin_users": admin_users,
218225
}
219226

220-
def get_config_with_secrets(self, config_key: str = "global") -> Optional[Dict[str, Any]]:
227+
def get_config_with_secrets(
228+
self, config_key: str = "global"
229+
) -> Optional[Dict[str, Any]]:
221230
"""Get OAuth2 config with actual secrets (for internal use only)."""
222231
return self.get_config(config_key, mask_secrets=False)
223232

@@ -242,7 +251,9 @@ def load_with_secrets(self) -> Optional[Dict[str, Any]]:
242251
"""Load OAuth2 config with actual secrets (for internal use only)."""
243252
return self.dao.get_config_with_secrets("global")
244253

245-
def save(self, enabled: bool, providers: List[Dict], admin_users: List[str]) -> bool:
254+
def save(
255+
self, enabled: bool, providers: List[Dict], admin_users: List[str]
256+
) -> bool:
246257
"""Save OAuth2 config to database."""
247258
try:
248259
self.dao.save_or_update(enabled, providers, admin_users, "global")

0 commit comments

Comments
 (0)