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:
@@ -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
|
||||
|
||||
|
||||
@@ -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 "请求失败"}
|
||||
|
||||
# ── 流式请求 ──────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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 天
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user