From 587933f6681b5ad433e7ea8d36844e7ffd17ffbe Mon Sep 17 00:00:00 2001 From: Jeason <1710884619@qq.com> Date: Thu, 2 Apr 2026 22:19:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E6=9E=B6=E6=9E=84=E7=BC=BA?= =?UTF-8?q?=E9=99=B7=206-12=20=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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,减少噪音 --- .env.example | 3 ++ src/core/auth_manager.py | 2 + src/core/database.py | 36 ++++++++++++++ src/core/embedding_client.py | 2 +- src/core/llm_client.py | 62 ++++++++++++++----------- src/knowledge_base/knowledge_manager.py | 4 +- src/web/app.py | 4 +- src/web/blueprints/auth.py | 1 + src/web/blueprints/feishu_bot.py | 4 +- src/web/static/js/dashboard.js | 14 ++++++ 10 files changed, 99 insertions(+), 33 deletions(-) diff --git a/.env.example b/.env.example index 17dfceb..beec820 100644 --- a/.env.example +++ b/.env.example @@ -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 diff --git a/src/core/auth_manager.py b/src/core/auth_manager.py index 8ac3168..c5df0fa 100644 --- a/src/core/auth_manager.py +++ b/src/core/auth_manager.py @@ -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 } diff --git a/src/core/database.py b/src/core/database.py index 5bdcd06..3afae7b 100644 --- a/src/core/database.py +++ b/src/core/database.py @@ -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: diff --git a/src/core/embedding_client.py b/src/core/embedding_client.py index 1a8f0d8..0100840 100644 --- a/src/core/embedding_client.py +++ b/src/core/embedding_client.py @@ -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): """延迟加载模型(首次调用时下载并加载)""" diff --git a/src/core/llm_client.py b/src/core/llm_client.py index 809d39f..80713c1 100644 --- a/src/core/llm_client.py +++ b/src/core/llm_client.py @@ -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 "请求失败"} # ── 流式请求 ────────────────────────────────────────── diff --git a/src/knowledge_base/knowledge_manager.py b/src/knowledge_base/knowledge_manager.py index a901efd..e489fb4 100644 --- a/src/knowledge_base/knowledge_manager.py +++ b/src/knowledge_base/knowledge_manager.py @@ -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: diff --git a/src/web/app.py b/src/web/app.py index b40d384..5431035 100644 --- a/src/web/app.py +++ b/src/web/app.py @@ -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 天 diff --git a/src/web/blueprints/auth.py b/src/web/blueprints/auth.py index 670cd4a..5ebe475 100644 --- a/src/web/blueprints/auth.py +++ b/src/web/blueprints/auth.py @@ -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 diff --git a/src/web/blueprints/feishu_bot.py b/src/web/blueprints/feishu_bot.py index e2c470a..ba8a3f5 100644 --- a/src/web/blueprints/feishu_bot.py +++ b/src/web/blueprints/feishu_bot.py @@ -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 diff --git a/src/web/static/js/dashboard.js b/src/web/static/js/dashboard.js index b458243..04bbcd0 100644 --- a/src/web/static/js/dashboard.js +++ b/src/web/static/js/dashboard.js @@ -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();