refactor: 架构缺陷 6-12 修复
6. SECRET_KEY 从硬编码改为环境变量读取,未设置时自动生成随机值 7. 登录时 session 存储 tenant_id,auth_manager 返回用户的 tenant_id 8. 前端共享状态集中声明并添加注释,标注每个状态由哪个模块管理 9. 数据库启动时自动检测并添加缺失的 tenant_id 列(SQLite ADD COLUMN 迁移) 10. Webhook handler 添加文档说明双通道互斥建议 11. LLM chat_completion 添加自动重试(max_retries=2),服务端错误和超时自动重试 12. 知识库向量化器和 Embedding 禁用日志从 INFO 降为 DEBUG,减少噪音
This commit is contained in:
@@ -67,6 +67,7 @@ class AuthManager:
|
||||
name_val = user.name
|
||||
role_val = user.role
|
||||
is_active_val = user.is_active
|
||||
tenant_id_val = user.tenant_id
|
||||
created_at_val = user.created_at
|
||||
last_login_val = datetime.now()
|
||||
|
||||
@@ -86,6 +87,7 @@ class AuthManager:
|
||||
'name': name_val,
|
||||
'role': role_val,
|
||||
'is_active': is_active_val,
|
||||
'tenant_id': tenant_id_val,
|
||||
'created_at': created_at_val,
|
||||
'last_login': last_login_val
|
||||
}
|
||||
|
||||
@@ -68,6 +68,9 @@ class DatabaseManager:
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
logger.info("数据库初始化成功")
|
||||
|
||||
# 运行 schema 迁移(处理字段变更)
|
||||
self._run_migrations()
|
||||
|
||||
# 确保默认租户存在
|
||||
self._ensure_default_tenant()
|
||||
|
||||
@@ -75,6 +78,39 @@ class DatabaseManager:
|
||||
logger.error(f"数据库初始化失败: {e}")
|
||||
raise
|
||||
|
||||
def _run_migrations(self):
|
||||
"""运行轻量级 schema 迁移(SQLite 兼容)"""
|
||||
try:
|
||||
session = self.SessionLocal()
|
||||
try:
|
||||
# 检查并添加缺失的列(SQLite 支持 ADD COLUMN)
|
||||
from sqlalchemy import inspect, text
|
||||
inspector = inspect(self.engine)
|
||||
migrations = [
|
||||
# (表名, 列名, SQL 类型默认值)
|
||||
('conversations', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||
('chat_sessions', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||
('work_orders', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||
('knowledge_entries', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||
('users', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||
('alerts', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||
('analytics', 'tenant_id', "VARCHAR(50) DEFAULT 'default'"),
|
||||
]
|
||||
for table_name, col_name, col_type in migrations:
|
||||
if table_name in inspector.get_table_names():
|
||||
existing_cols = [c['name'] for c in inspector.get_columns(table_name)]
|
||||
if col_name not in existing_cols:
|
||||
session.execute(text(f"ALTER TABLE {table_name} ADD COLUMN {col_name} {col_type}"))
|
||||
logger.info(f"迁移: {table_name} 添加列 {col_name}")
|
||||
session.commit()
|
||||
except Exception as e:
|
||||
session.rollback()
|
||||
logger.warning(f"Schema 迁移失败(不影响启动): {e}")
|
||||
finally:
|
||||
session.close()
|
||||
except Exception as e:
|
||||
logger.warning(f"Schema 迁移检查失败: {e}")
|
||||
|
||||
def _ensure_default_tenant(self):
|
||||
"""确保默认租户记录存在"""
|
||||
try:
|
||||
|
||||
@@ -32,7 +32,7 @@ class EmbeddingClient:
|
||||
if self.enabled:
|
||||
logger.info(f"Embedding 客户端初始化: model={self.model_name} (本地模式)")
|
||||
else:
|
||||
logger.info("Embedding 功能已禁用,将使用关键词匹配降级")
|
||||
logger.debug("Embedding 功能已禁用,将使用关键词匹配降级")
|
||||
|
||||
def _get_model(self):
|
||||
"""延迟加载模型(首次调用时下载并加载)"""
|
||||
|
||||
@@ -42,38 +42,46 @@ class LLMClient:
|
||||
messages: List[Dict[str, str]],
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 1000,
|
||||
max_retries: int = 2,
|
||||
**kwargs,
|
||||
) -> Dict[str, Any]:
|
||||
"""标准聊天补全(非流式)"""
|
||||
try:
|
||||
url = f"{self.base_url}/chat/completions"
|
||||
payload = {
|
||||
"model": self.model_name,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"stream": False,
|
||||
}
|
||||
"""标准聊天补全(非流式),支持自动重试"""
|
||||
url = f"{self.base_url}/chat/completions"
|
||||
payload = {
|
||||
"model": self.model_name,
|
||||
"messages": messages,
|
||||
"temperature": temperature,
|
||||
"max_tokens": max_tokens,
|
||||
"stream": False,
|
||||
}
|
||||
|
||||
response = requests.post(
|
||||
url, headers=self.headers, json=payload, timeout=self.timeout
|
||||
)
|
||||
last_error = None
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
response = requests.post(
|
||||
url, headers=self.headers, json=payload, timeout=self.timeout
|
||||
)
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
elif response.status_code >= 500:
|
||||
last_error = f"API 服务端错误: {response.status_code}"
|
||||
logger.warning(f"LLM API 第{attempt+1}次请求失败({response.status_code}),{'重试中...' if attempt < max_retries else '放弃'}")
|
||||
continue
|
||||
else:
|
||||
logger.error(f"LLM API 失败: {response.status_code} - {response.text}")
|
||||
return {"error": f"API请求失败: {response.status_code}"}
|
||||
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
else:
|
||||
logger.error(f"LLM API 失败: {response.status_code} - {response.text}")
|
||||
return {"error": f"API请求失败: {response.status_code}"}
|
||||
except requests.exceptions.Timeout:
|
||||
last_error = "请求超时"
|
||||
logger.warning(f"LLM API 第{attempt+1}次超时,{'重试中...' if attempt < max_retries else '放弃'}")
|
||||
except requests.exceptions.RequestException as e:
|
||||
last_error = str(e)
|
||||
logger.warning(f"LLM API 第{attempt+1}次异常: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"LLM 未知错误: {e}")
|
||||
return {"error": f"未知错误: {str(e)}"}
|
||||
|
||||
except requests.exceptions.Timeout:
|
||||
logger.error("LLM API 超时")
|
||||
return {"error": "请求超时"}
|
||||
except requests.exceptions.RequestException as e:
|
||||
logger.error(f"LLM API 异常: {e}")
|
||||
return {"error": f"请求异常: {str(e)}"}
|
||||
except Exception as e:
|
||||
logger.error(f"LLM 未知错误: {e}")
|
||||
return {"error": f"未知错误: {str(e)}"}
|
||||
return {"error": last_error or "请求失败"}
|
||||
|
||||
# ── 流式请求 ──────────────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user