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.
|
# The host the web server will bind to.
|
||||||
SERVER_HOST=0.0.0.0
|
SERVER_HOST=0.0.0.0
|
||||||
|
|
||||||
|
# Flask session 加密密钥(生产环境必须设置固定值,否则每次重启 session 失效)
|
||||||
|
SECRET_KEY=your-random-secret-key-here
|
||||||
|
|
||||||
# The port for the main Flask web server.
|
# The port for the main Flask web server.
|
||||||
SERVER_PORT=5001
|
SERVER_PORT=5001
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class AuthManager:
|
|||||||
name_val = user.name
|
name_val = user.name
|
||||||
role_val = user.role
|
role_val = user.role
|
||||||
is_active_val = user.is_active
|
is_active_val = user.is_active
|
||||||
|
tenant_id_val = user.tenant_id
|
||||||
created_at_val = user.created_at
|
created_at_val = user.created_at
|
||||||
last_login_val = datetime.now()
|
last_login_val = datetime.now()
|
||||||
|
|
||||||
@@ -86,6 +87,7 @@ class AuthManager:
|
|||||||
'name': name_val,
|
'name': name_val,
|
||||||
'role': role_val,
|
'role': role_val,
|
||||||
'is_active': is_active_val,
|
'is_active': is_active_val,
|
||||||
|
'tenant_id': tenant_id_val,
|
||||||
'created_at': created_at_val,
|
'created_at': created_at_val,
|
||||||
'last_login': last_login_val
|
'last_login': last_login_val
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -68,6 +68,9 @@ class DatabaseManager:
|
|||||||
Base.metadata.create_all(bind=self.engine)
|
Base.metadata.create_all(bind=self.engine)
|
||||||
logger.info("数据库初始化成功")
|
logger.info("数据库初始化成功")
|
||||||
|
|
||||||
|
# 运行 schema 迁移(处理字段变更)
|
||||||
|
self._run_migrations()
|
||||||
|
|
||||||
# 确保默认租户存在
|
# 确保默认租户存在
|
||||||
self._ensure_default_tenant()
|
self._ensure_default_tenant()
|
||||||
|
|
||||||
@@ -75,6 +78,39 @@ class DatabaseManager:
|
|||||||
logger.error(f"数据库初始化失败: {e}")
|
logger.error(f"数据库初始化失败: {e}")
|
||||||
raise
|
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):
|
def _ensure_default_tenant(self):
|
||||||
"""确保默认租户记录存在"""
|
"""确保默认租户记录存在"""
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class EmbeddingClient:
|
|||||||
if self.enabled:
|
if self.enabled:
|
||||||
logger.info(f"Embedding 客户端初始化: model={self.model_name} (本地模式)")
|
logger.info(f"Embedding 客户端初始化: model={self.model_name} (本地模式)")
|
||||||
else:
|
else:
|
||||||
logger.info("Embedding 功能已禁用,将使用关键词匹配降级")
|
logger.debug("Embedding 功能已禁用,将使用关键词匹配降级")
|
||||||
|
|
||||||
def _get_model(self):
|
def _get_model(self):
|
||||||
"""延迟加载模型(首次调用时下载并加载)"""
|
"""延迟加载模型(首次调用时下载并加载)"""
|
||||||
|
|||||||
@@ -42,38 +42,46 @@ class LLMClient:
|
|||||||
messages: List[Dict[str, str]],
|
messages: List[Dict[str, str]],
|
||||||
temperature: float = 0.7,
|
temperature: float = 0.7,
|
||||||
max_tokens: int = 1000,
|
max_tokens: int = 1000,
|
||||||
|
max_retries: int = 2,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""标准聊天补全(非流式)"""
|
"""标准聊天补全(非流式),支持自动重试"""
|
||||||
try:
|
url = f"{self.base_url}/chat/completions"
|
||||||
url = f"{self.base_url}/chat/completions"
|
payload = {
|
||||||
payload = {
|
"model": self.model_name,
|
||||||
"model": self.model_name,
|
"messages": messages,
|
||||||
"messages": messages,
|
"temperature": temperature,
|
||||||
"temperature": temperature,
|
"max_tokens": max_tokens,
|
||||||
"max_tokens": max_tokens,
|
"stream": False,
|
||||||
"stream": False,
|
}
|
||||||
}
|
|
||||||
|
|
||||||
response = requests.post(
|
last_error = None
|
||||||
url, headers=self.headers, json=payload, timeout=self.timeout
|
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:
|
except requests.exceptions.Timeout:
|
||||||
return response.json()
|
last_error = "请求超时"
|
||||||
else:
|
logger.warning(f"LLM API 第{attempt+1}次超时,{'重试中...' if attempt < max_retries else '放弃'}")
|
||||||
logger.error(f"LLM API 失败: {response.status_code} - {response.text}")
|
except requests.exceptions.RequestException as e:
|
||||||
return {"error": f"API请求失败: {response.status_code}"}
|
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:
|
return {"error": last_error or "请求失败"}
|
||||||
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)}"}
|
|
||||||
|
|
||||||
# ── 流式请求 ──────────────────────────────────────────
|
# ── 流式请求 ──────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class KnowledgeManager:
|
|||||||
def _load_vectorizer(self):
|
def _load_vectorizer(self):
|
||||||
"""加载向量化器"""
|
"""加载向量化器"""
|
||||||
try:
|
try:
|
||||||
logger.info("正在初始化知识库向量化器...")
|
logger.debug("正在初始化知识库向量化器...")
|
||||||
with db_manager.get_session() as session:
|
with db_manager.get_session() as session:
|
||||||
entries = session.query(KnowledgeEntry).filter(
|
entries = session.query(KnowledgeEntry).filter(
|
||||||
KnowledgeEntry.is_active == True
|
KnowledgeEntry.is_active == True
|
||||||
@@ -46,7 +46,7 @@ class KnowledgeManager:
|
|||||||
if entries:
|
if entries:
|
||||||
texts = [entry.question + " " + entry.answer for entry in entries]
|
texts = [entry.question + " " + entry.answer for entry in entries]
|
||||||
self.vectorizer.fit(texts)
|
self.vectorizer.fit(texts)
|
||||||
logger.info(f"向量化器加载成功: 共处理 {len(entries)} 个知识条目")
|
logger.debug(f"向量化器加载成功: 共处理 {len(entries)} 个知识条目")
|
||||||
else:
|
else:
|
||||||
logger.warning("知识库尚无活跃条目,向量化器将保持空状态")
|
logger.warning("知识库尚无活跃条目,向量化器将保持空状态")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -70,8 +70,8 @@ werkzeug_logger.addFilter(MonitoringLogFilter())
|
|||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
CORS(app)
|
CORS(app)
|
||||||
|
|
||||||
# 配置 session secret key(用于加密 session)
|
# 配置 session secret key(从环境变量读取,不硬编码)
|
||||||
app.config['SECRET_KEY'] = 'tsp-assistant-secret-key-change-in-production'
|
app.config['SECRET_KEY'] = os.environ.get('SECRET_KEY', os.urandom(32).hex())
|
||||||
app.config['SESSION_TYPE'] = 'filesystem'
|
app.config['SESSION_TYPE'] = 'filesystem'
|
||||||
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # session 有效期 7 天
|
app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=7) # session 有效期 7 天
|
||||||
|
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ def login():
|
|||||||
# 存储到session
|
# 存储到session
|
||||||
session['user_id'] = user_data['id']
|
session['user_id'] = user_data['id']
|
||||||
session['username'] = user_data['username']
|
session['username'] = user_data['username']
|
||||||
|
session['tenant_id'] = user_data.get('tenant_id', 'default')
|
||||||
session['user_info'] = user_data
|
session['user_info'] = user_data
|
||||||
session['token'] = token
|
session['token'] = token
|
||||||
|
|
||||||
|
|||||||
@@ -193,7 +193,9 @@ def _process_message_in_background(app, event_data: dict):
|
|||||||
@feishu_bot_bp.route('/event', methods=['POST'])
|
@feishu_bot_bp.route('/event', methods=['POST'])
|
||||||
def handle_feishu_event():
|
def handle_feishu_event():
|
||||||
"""
|
"""
|
||||||
接收并处理飞书事件回调
|
接收并处理飞书事件回调(Webhook 模式)。
|
||||||
|
如果系统同时运行了长连接模式,消息去重机制会自动跳过已处理的消息。
|
||||||
|
建议生产环境只启用一种模式(长连接 OR Webhook),避免重复处理。
|
||||||
"""
|
"""
|
||||||
# 1. 解析请求
|
# 1. 解析请求
|
||||||
data = request.json
|
data = request.json
|
||||||
|
|||||||
@@ -69,24 +69,38 @@ function updatePageLanguage(lang) {
|
|||||||
|
|
||||||
class TSPDashboard {
|
class TSPDashboard {
|
||||||
constructor() {
|
constructor() {
|
||||||
|
// ===== 共享状态(所有模块通过 this.xxx 访问)=====
|
||||||
|
// 导航
|
||||||
this.currentTab = 'dashboard';
|
this.currentTab = 'dashboard';
|
||||||
|
// 图表实例(由 system.js 管理)
|
||||||
this.charts = {};
|
this.charts = {};
|
||||||
|
// 定时器(由 core 管理)
|
||||||
this.refreshIntervals = {};
|
this.refreshIntervals = {};
|
||||||
|
// WebSocket 连接
|
||||||
this.websocket = null;
|
this.websocket = null;
|
||||||
|
// 当前对话会话 ID(由 chat.js 管理)
|
||||||
this.sessionId = null;
|
this.sessionId = null;
|
||||||
|
// Agent 模式开关(由 agent.js 管理)
|
||||||
this.isAgentMode = true;
|
this.isAgentMode = true;
|
||||||
|
// 语言
|
||||||
this.currentLanguage = localStorage.getItem('preferred-language') || 'zh';
|
this.currentLanguage = localStorage.getItem('preferred-language') || 'zh';
|
||||||
|
// 前端缓存
|
||||||
this.cache = new Map();
|
this.cache = new Map();
|
||||||
this.cacheTimeout = 30000;
|
this.cacheTimeout = 30000;
|
||||||
|
// 智能更新时间戳
|
||||||
this.lastUpdateTimes = { alerts: 0, workorders: 0, health: 0, analytics: 0 };
|
this.lastUpdateTimes = { alerts: 0, workorders: 0, health: 0, analytics: 0 };
|
||||||
this.updateThresholds = { alerts: 10000, workorders: 30000, health: 30000, analytics: 60000 };
|
this.updateThresholds = { alerts: 10000, workorders: 30000, health: 30000, analytics: 60000 };
|
||||||
this.isPageVisible = true;
|
this.isPageVisible = true;
|
||||||
|
// 租户视图状态(由 knowledge.js / conversations.js 管理)
|
||||||
this.knowledgeCurrentTenantId = null;
|
this.knowledgeCurrentTenantId = null;
|
||||||
this.conversationCurrentTenantId = null;
|
this.conversationCurrentTenantId = null;
|
||||||
|
// 分页
|
||||||
this.paginationConfig = {
|
this.paginationConfig = {
|
||||||
defaultPageSize: 10, pageSizeOptions: [5, 10, 20, 50], maxVisiblePages: 5,
|
defaultPageSize: 10, pageSizeOptions: [5, 10, 20, 50], maxVisiblePages: 5,
|
||||||
currentKnowledgePage: 1, currentWorkOrderPage: 1, currentConversationPage: 1
|
currentKnowledgePage: 1, currentWorkOrderPage: 1, currentConversationPage: 1
|
||||||
};
|
};
|
||||||
|
// ===== 共享状态结束 =====
|
||||||
|
|
||||||
this.init();
|
this.init();
|
||||||
this.restorePageState();
|
this.restorePageState();
|
||||||
this.initLanguage();
|
this.initLanguage();
|
||||||
|
|||||||
Reference in New Issue
Block a user