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:
2026-04-02 22:19:56 +08:00
parent 61ef86d779
commit 587933f668
10 changed files with 99 additions and 33 deletions

View File

@@ -8,6 +8,9 @@
# The host the web server will bind to.
SERVER_HOST=0.0.0.0
# Flask session 加密密钥(生产环境必须设置固定值,否则每次重启 session 失效)
SECRET_KEY=your-random-secret-key-here
# The port for the main Flask web server.
SERVER_PORT=5001

View File

@@ -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
}

View File

@@ -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:

View File

@@ -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):
"""延迟加载模型(首次调用时下载并加载)"""

View File

@@ -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 "请求失败"}
# ── 流式请求 ──────────────────────────────────────────

View File

@@ -37,7 +37,7 @@ class KnowledgeManager:
def _load_vectorizer(self):
"""加载向量化器"""
try:
logger.info("正在初始化知识库向量化器...")
logger.debug("正在初始化知识库向量化器...")
with db_manager.get_session() as session:
entries = session.query(KnowledgeEntry).filter(
KnowledgeEntry.is_active == True
@@ -46,7 +46,7 @@ class KnowledgeManager:
if entries:
texts = [entry.question + " " + entry.answer for entry in entries]
self.vectorizer.fit(texts)
logger.info(f"向量化器加载成功: 共处理 {len(entries)} 个知识条目")
logger.debug(f"向量化器加载成功: 共处理 {len(entries)} 个知识条目")
else:
logger.warning("知识库尚无活跃条目,向量化器将保持空状态")
except Exception as e:

View File

@@ -70,8 +70,8 @@ werkzeug_logger.addFilter(MonitoringLogFilter())
app = Flask(__name__)
CORS(app)
# 配置 session secret key用于加密 session
app.config['SECRET_KEY'] = 'tsp-assistant-secret-key-change-in-production'
# 配置 session secret key从环境变量读取,不硬编码
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', os.urandom(32).hex())
app.config['SESSION_TYPE'] = 'filesystem'
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # session 有效期 7 天

View File

@@ -34,6 +34,7 @@ def login():
# 存储到session
session['user_id'] = user_data['id']
session['username'] = user_data['username']
session['tenant_id'] = user_data.get('tenant_id', 'default')
session['user_info'] = user_data
session['token'] = token

View File

@@ -193,7 +193,9 @@ def _process_message_in_background(app, event_data: dict):
@feishu_bot_bp.route('/event', methods=['POST'])
def handle_feishu_event():
"""
接收并处理飞书事件回调
接收并处理飞书事件回调Webhook 模式)。
如果系统同时运行了长连接模式,消息去重机制会自动跳过已处理的消息。
建议生产环境只启用一种模式(长连接 OR Webhook避免重复处理。
"""
# 1. 解析请求
data = request.json

View File

@@ -69,24 +69,38 @@ function updatePageLanguage(lang) {
class TSPDashboard {
constructor() {
// ===== 共享状态(所有模块通过 this.xxx 访问)=====
// 导航
this.currentTab = 'dashboard';
// 图表实例(由 system.js 管理)
this.charts = {};
// 定时器(由 core 管理)
this.refreshIntervals = {};
// WebSocket 连接
this.websocket = null;
// 当前对话会话 ID由 chat.js 管理)
this.sessionId = null;
// Agent 模式开关(由 agent.js 管理)
this.isAgentMode = true;
// 语言
this.currentLanguage = localStorage.getItem('preferred-language') || 'zh';
// 前端缓存
this.cache = new Map();
this.cacheTimeout = 30000;
// 智能更新时间戳
this.lastUpdateTimes = { alerts: 0, workorders: 0, health: 0, analytics: 0 };
this.updateThresholds = { alerts: 10000, workorders: 30000, health: 30000, analytics: 60000 };
this.isPageVisible = true;
// 租户视图状态(由 knowledge.js / conversations.js 管理)
this.knowledgeCurrentTenantId = null;
this.conversationCurrentTenantId = null;
// 分页
this.paginationConfig = {
defaultPageSize: 10, pageSizeOptions: [5, 10, 20, 50], maxVisiblePages: 5,
currentKnowledgePage: 1, currentWorkOrderPage: 1, currentConversationPage: 1
};
// ===== 共享状态结束 =====
this.init();
this.restorePageState();
this.initLanguage();