fix: 飞书长连接改用 ThreadPoolExecutor 处理消息

- 用 ThreadPoolExecutor(max_workers=5) 替代单次 threading.Thread
- 支持并发处理多条消息,避免排队阻塞
- 添加消息序号日志,方便追踪消息接收和处理
- _process_message_safe 包装确保异常不会导致线程崩溃
- 如果消息确实没被 SDK 推送,日志里不会有对应的 #N 记录
This commit is contained in:
2026-04-02 16:24:58 +08:00
parent 18fb3155ba
commit d6c87683af
9 changed files with 106 additions and 89 deletions

View File

@@ -25,7 +25,7 @@
- **知识库集成**: 基于TF-IDF和余弦相似度的智能检索 - **知识库集成**: 基于TF-IDF和余弦相似度的智能检索
- **自定义提示词**: 支持飞书同步和实时对话不同场景的LLM提示词 - **自定义提示词**: 支持飞书同步和实时对话不同场景的LLM提示词
### 📊 数据驱动分析 ### 数据驱动分析
- **真实数据**: 基于数据库的真实性能趋势分析 - **真实数据**: 基于数据库的真实性能趋势分析
- **多维度统计**: 工单、预警、满意度、性能指标 - **多维度统计**: 工单、预警、满意度、性能指标
- **可视化展示**: Chart.js图表直观的数据呈现 - **可视化展示**: Chart.js图表直观的数据呈现
@@ -92,7 +92,7 @@
- **向量化检索**: TF-IDF + 余弦相似度搜索 - **向量化检索**: TF-IDF + 余弦相似度搜索
- **质量验证**: 支持知识条目验证和置信度设置 - **质量验证**: 支持知识条目验证和置信度设置
### 5. 数据分析 📊 ### 5. 数据分析
- **实时趋势**: 基于真实数据的性能趋势分析 - **实时趋势**: 基于真实数据的性能趋势分析
- **多维度统计**: 工单、预警、满意度等关键指标 - **多维度统计**: 工单、预警、满意度等关键指标
- **系统健康**: CPU、内存、响应时间监控 - **系统健康**: CPU、内存、响应时间监控
@@ -289,7 +289,7 @@ python version.py changelog --message "新功能描述"
python version.py tag --message "Release v1.3.0" python version.py tag --message "Release v1.3.0"
``` ```
## 📊 系统监控 ## 系统监控
### 健康检查 ### 健康检查
- **API状态**: `/api/health` - **API状态**: `/api/health`
@@ -393,38 +393,38 @@ TZ=Asia/Shanghai
- 🔧 **知识库搜索修复**: 简化搜索算法,提升检索准确率 - 🔧 **知识库搜索修复**: 简化搜索算法,提升检索准确率
- 🔧 **批量删除优化**: 修复外键约束和缓存问题 - 🔧 **批量删除优化**: 修复外键约束和缓存问题
- 🔧 **日志编码修复**: 解决中文乱码问题 - 🔧 **日志编码修复**: 解决中文乱码问题
- 📊 **可视化增强**: 修复预警、性能、满意度图表显示 - **可视化增强**: 修复预警、性能、满意度图表显示
- 📚 **文档更新**: 完整的Docker部署和使用指南 - 📚 **文档更新**: 完整的Docker部署和使用指南
### v1.4.0 (2025-09-19) ### v1.4.0 (2025-09-19)
- 飞书集成功能:支持飞书多维表格数据同步 - 飞书集成功能:支持飞书多维表格数据同步
- 页面功能合并:飞书同步页面合并到主仪表板 - 页面功能合并:飞书同步页面合并到主仪表板
- 数据库架构优化:扩展工单表字段,支持飞书数据 - 数据库架构优化:扩展工单表字段,支持飞书数据
- 代码重构优化:大文件拆分,降低运行风险 - 代码重构优化:大文件拆分,降低运行风险
- 字段映射完善:智能映射飞书字段到本地数据库 - 字段映射完善:智能映射飞书字段到本地数据库
- 数据库初始化改进:集成字段迁移到初始化流程 - 数据库初始化改进:集成字段迁移到初始化流程
### v1.3.0 (2025-09-17) ### v1.3.0 (2025-09-17)
- 数据库架构优化MySQL主数据库+SQLite备份系统 - 数据库架构优化MySQL主数据库+SQLite备份系统
- 工单详情API修复解决数据库会话管理问题 - 工单详情API修复解决数据库会话管理问题
- 备份管理系统自动备份MySQL数据到SQLite - 备份管理系统自动备份MySQL数据到SQLite
- 数据库状态监控实时监控MySQL和SQLite状态 - 数据库状态监控实时监控MySQL和SQLite状态
- 备份管理API支持数据备份和恢复操作 - 备份管理API支持数据备份和恢复操作
### v1.2.0 (2025-09-16) ### v1.2.0 (2025-09-16)
- 系统设置扩展API管理、模型参数配置、端口管理 - 系统设置扩展API管理、模型参数配置、端口管理
- 真实数据分析:修复性能趋势图表显示问题 - 真实数据分析:修复性能趋势图表显示问题
- 工单AI建议功能智能生成处理建议 - 工单AI建议功能智能生成处理建议
- 知识库搜索优化:提升检索准确率 - 知识库搜索优化:提升检索准确率
- Agent管理改进工具使用统计和自定义工具 - Agent管理改进工具使用统计和自定义工具
### v1.1.0 (2025-09-16) ### v1.1.0 (2025-09-16)
- 工单AI建议功能 - 工单AI建议功能
- 知识库搜索优化 - 知识库搜索优化
- Agent管理改进 - Agent管理改进
### v1.0.0 (2024-01-01) ### v1.0.0 (2024-01-01)
- 初始版本发布 - 初始版本发布
## 📄 许可证 ## 📄 许可证

Binary file not shown.

View File

@@ -1,6 +1,6 @@
# 飞书长连接模式使用指南 # 飞书长连接模式使用指南
> **已验证可用** - 2026-02-11 > **已验证可用** - 2026-02-11
> >
> 本文档介绍如何使用飞书官方 SDK 的长连接模式,无需公网域名即可接入飞书机器人。 > 本文档介绍如何使用飞书官方 SDK 的长连接模式,无需公网域名即可接入飞书机器人。
@@ -8,14 +8,14 @@
飞书官方 SDK 提供了**事件订阅 2.0(长连接模式)**,相比传统的 webhook 模式有以下优势: 飞书官方 SDK 提供了**事件订阅 2.0(长连接模式)**,相比传统的 webhook 模式有以下优势:
| 特性 | Webhook 模式(旧) | 长连接模式(新) | | 特性 | Webhook 模式(旧) | 长连接模式(新) |
|------|-------------------|-------------------| |------|-------------------|-------------------|
| 公网域名 | 必需 | 不需要 | | 公网域名 | 必需 | 不需要 |
| SSL 证书 | 必需 | 不需要 | | SSL 证书 | 必需 | 不需要 |
| 回调配置 | 需要配置 | 不需要 | | 回调配置 | 需要配置 | 不需要 |
| 内网部署 | 不支持 | 支持 | | 内网部署 | 不支持 | 支持 |
| 实时性 | 中等HTTP 轮询) | WebSocket | | 实时性 | 中等HTTP 轮询) | 高WebSocket |
| 稳定性 | 中等 | 高(自动重连) | | 稳定性 | 中等 | 高(自动重连) |
--- ---
@@ -61,12 +61,12 @@ python init_database.py
访问 [飞书开放平台](https://open.feishu.cn/app),为您的应用添加以下权限: 访问 [飞书开放平台](https://open.feishu.cn/app),为您的应用添加以下权限:
**必需权限:** **必需权限:**
- `im:message` - 获取与发送单聊、群组消息 - `im:message` - 获取与发送单聊、群组消息
- `im:message:send_as_bot` - 以应用的身份发消息 - `im:message:send_as_bot` - 以应用的身份发消息
- `im:chat` - 获取群组信息 - `im:chat` - 获取群组信息
**可选权限(用于工单同步):** **可选权限(用于工单同步):**
- `bitable:app` - 查看、评论、编辑和管理多维表格 - `bitable:app` - 查看、评论、编辑和管理多维表格
**注意:** 添加权限后需要重新发布应用版本。 **注意:** 添加权限后需要重新发布应用版本。
@@ -94,7 +94,7 @@ python start_feishu_bot.py
- 日志级别: INFO - 日志级别: INFO
🔌 启动模式: 事件订阅 2.0(长连接) 🔌 启动模式: 事件订阅 2.0(长连接)
优势: 优势:
- 无需公网域名 - 无需公网域名
- 无需配置 webhook - 无需配置 webhook
- 自动重连 - 自动重连
@@ -107,7 +107,7 @@ python start_feishu_bot.py
- App ID: cli_xxxxxxxxxxxxx - App ID: cli_xxxxxxxxxxxxx
- 模式: 事件订阅 2.0(长连接) - 模式: 事件订阅 2.0(长连接)
- 无需公网域名和 webhook 配置 - 无需公网域名和 webhook 配置
飞书长连接服务初始化成功 飞书长连接服务初始化成功
``` ```
### 方式 2: 在代码中集成 ### 方式 2: 在代码中集成
@@ -175,10 +175,10 @@ app.run(...)
- 原始内容: @TSP助手 车辆无法连接网络 - 原始内容: @TSP助手 车辆无法连接网络
- 清理后内容: 车辆无法连接网络 - 清理后内容: 车辆无法连接网络
🔑 会话用户标识: feishu_oc_xxxxxxxxxxxxx_ou_xxxxxxxxxxxxx 🔑 会话用户标识: feishu_oc_xxxxxxxxxxxxx_ou_xxxxxxxxxxxxx
🆕 为用户 ou_xxxxxxxxxxxxx 在群聊 oc_xxxxxxxxxxxxx 创建新会话: session_xxxxxxxxxxxxx 为用户 ou_xxxxxxxxxxxxx 在群聊 oc_xxxxxxxxxxxxx 创建新会话: session_xxxxxxxxxxxxx
🤖 调用 TSP Assistant 处理消息... 🤖 调用 TSP Assistant 处理消息...
📤 准备发送回复 (长度: 156) 📤 准备发送回复 (长度: 156)
成功回复消息: om_xxxxxxxxxxxxx 成功回复消息: om_xxxxxxxxxxxxx
``` ```
--- ---
@@ -195,12 +195,12 @@ app.run(...)
``` ```
**缺点:** **缺点:**
- 需要公网域名 - 需要公网域名
- 需要配置 SSL - 需要配置 SSL
- 内网无法部署 - 内网无法部署
- 需要在飞书后台配置回调地址 - 需要在飞书后台配置回调地址
### 新模式(长连接)- 推荐 ### 新模式(长连接)- 推荐
**文件:** `src/integrations/feishu_longconn_service.py` **文件:** `src/integrations/feishu_longconn_service.py`
@@ -210,16 +210,16 @@ app.run(...)
``` ```
**优点:** **优点:**
- 无需公网域名 - 无需公网域名
- 无需 SSL 证书 - 无需 SSL 证书
- 内网可部署 - 内网可部署
- 无需配置回调地址 - 无需配置回调地址
- 自动重连 - 自动重连
- 更稳定 - 更稳定
--- ---
## 📊 架构说明 ## 架构说明
``` ```
┌─────────────────────────────────────────────────────────────┐ ┌─────────────────────────────────────────────────────────────┐
@@ -332,7 +332,7 @@ python3 -m pip install --upgrade certifi
3. **验证修复** 3. **验证修复**
```bash ```bash
python3 -c "import urllib.request; urllib.request.urlopen('https://open.feishu.cn', timeout=5); print(' SSL 验证成功')" python3 -c "import urllib.request; urllib.request.urlopen('https://open.feishu.cn', timeout=5); print(' SSL 验证成功')"
``` ```
**解决方案Linux/Windows** **解决方案Linux/Windows**
@@ -343,11 +343,11 @@ pip3 install --upgrade certifi
### Q2: 启动后没有收到消息? ### Q2: 启动后没有收到消息?
**检查清单:** **检查清单:**
1. 确认飞书应用权限已配置(`im:message` 1. 确认飞书应用权限已配置(`im:message`
2. 确认应用已发布最新版本 2. 确认应用已发布最新版本
3. 确认机器人已添加到群聊 3. 确认机器人已添加到群聊
4. 查看日志是否有连接成功的提示 4. 查看日志是否有连接成功的提示
5. 确认没有 SSL 证书错误(见 Q1 5. 确认没有 SSL 证书错误(见 Q1
### Q3: 提示"权限不足" ### Q3: 提示"权限不足"
@@ -379,7 +379,7 @@ pip3 install --upgrade certifi
现在您的 TSP Assistant 已经支持**飞书官方 SDK 的长连接模式** 现在您的 TSP Assistant 已经支持**飞书官方 SDK 的长连接模式**
**核心优势:** **核心优势:**
- 无需公网域名 - 无需公网域名
- 无需 webhook 配置 - 无需 webhook 配置
- 内网可部署 - 内网可部署
@@ -387,7 +387,7 @@ pip3 install --upgrade certifi
- 群聊隔离 - 群聊隔离
- 完整的工单管理和 AI 分析功能 - 完整的工单管理和 AI 分析功能
**使用方式:** **使用方式:**
```bash ```bash
python start_feishu_bot.py python start_feishu_bot.py
``` ```

View File

@@ -6,7 +6,9 @@
import logging import logging
import json import json
import threading import threading
import queue
from typing import Optional from typing import Optional
from concurrent.futures import ThreadPoolExecutor
import lark_oapi as lark import lark_oapi as lark
from lark_oapi.api.im.v1 import P2ImMessageReceiveV1, ReplyMessageRequest, ReplyMessageRequestBody from lark_oapi.api.im.v1 import P2ImMessageReceiveV1, ReplyMessageRequest, ReplyMessageRequestBody
@@ -38,7 +40,7 @@ class FeishuLongConnService:
.log_level(lark.LogLevel.DEBUG) \ .log_level(lark.LogLevel.DEBUG) \
.build() .build()
logger.info(" 飞书客户端创建成功") logger.info(" 飞书客户端创建成功")
# 创建事件处理器 # 创建事件处理器
self.event_handler = lark.EventDispatcherHandler.builder( self.event_handler = lark.EventDispatcherHandler.builder(
@@ -46,16 +48,31 @@ class FeishuLongConnService:
).register_p2_im_message_receive_v1(self._handle_message) \ ).register_p2_im_message_receive_v1(self._handle_message) \
.build() .build()
logger.info(" 飞书事件处理器创建成功") logger.info(" 飞书事件处理器创建成功")
logger.info(" 飞书长连接服务初始化完成") logger.info(" 飞书长连接服务初始化完成")
# 消息处理线程池(最多同时处理 5 条消息)
self._executor = ThreadPoolExecutor(max_workers=5, thread_name_prefix="feishu_msg")
self._msg_count = 0
def _handle_message(self, data: P2ImMessageReceiveV1) -> None: def _handle_message(self, data: P2ImMessageReceiveV1) -> None:
"""收到消息事件,立即派发到后台线程处理,避免阻塞 SDK 事件循环""" """收到消息事件,立即提交到线程池,确保 SDK 回调快速返回"""
threading.Thread( self._msg_count += 1
target=self._process_message, msg_no = self._msg_count
args=(data,), logger.info(f"📨 收到飞书消息事件 #{msg_no},提交到处理队列")
daemon=True self._executor.submit(self._process_message_safe, data, msg_no)
).start()
def _process_message_safe(self, data: P2ImMessageReceiveV1, msg_no: int) -> None:
"""安全包装,确保异常不会导致线程崩溃"""
try:
self._process_message(data)
except Exception as e:
logger.error(f"❌ 消息 #{msg_no} 处理异常: {e}", exc_info=True)
try:
mid = data.event.message.message_id
self._reply_message(mid, "抱歉,系统遇到了一些问题,请稍后重试。")
except:
pass
def _process_message(self, data: P2ImMessageReceiveV1) -> None: def _process_message(self, data: P2ImMessageReceiveV1) -> None:
""" """
@@ -131,7 +148,7 @@ class FeishuLongConnService:
content_json = json.loads(content) content_json = json.loads(content)
text_content = content_json.get("text", "").strip() text_content = content_json.get("text", "").strip()
except json.JSONDecodeError as e: except json.JSONDecodeError as e:
logger.error(f"解析消息内容失败: {e}") logger.error(f"解析消息内容失败: {e}")
return return
logger.info(f"📝 文本内容: {text_content}") logger.info(f"📝 文本内容: {text_content}")
@@ -151,11 +168,11 @@ class FeishuLongConnService:
break break
if not text_content: if not text_content:
logger.warning(f"⚠️ 移除@后内容为空,回复提示") logger.warning(f" 移除@后内容为空,回复提示")
self._reply_message(message_id, "您好!请问有什么可以帮助您的吗?") self._reply_message(message_id, "您好!请问有什么可以帮助您的吗?")
return return
logger.info(f" 清理后内容: {text_content}") logger.info(f" 清理后内容: {text_content}")
# 构造会话用户ID群聊隔离 # 构造会话用户ID群聊隔离
session_user_id = f"feishu_{chat_id}_{sender_id}" session_user_id = f"feishu_{chat_id}_{sender_id}"
@@ -178,7 +195,7 @@ class FeishuLongConnService:
# 更新会话的 tenant_id群可能重新绑定了租户 # 更新会话的 tenant_id群可能重新绑定了租户
if session_id in chat_manager.active_sessions: if session_id in chat_manager.active_sessions:
chat_manager.active_sessions[session_id]['tenant_id'] = tenant_id chat_manager.active_sessions[session_id]['tenant_id'] = tenant_id
logger.info(f" 找到已有会话: {session_id}") logger.info(f" 找到已有会话: {session_id}")
break break
# 如果没有会话,创建新会话 # 如果没有会话,创建新会话
@@ -188,7 +205,7 @@ class FeishuLongConnService:
work_order_id=None, work_order_id=None,
tenant_id=tenant_id tenant_id=tenant_id
) )
logger.info(f"🆕 创建新会话: {session_id}, 用户={sender_name}({sender_id}), 租户={tenant_id}") logger.info(f" 创建新会话: {session_id}, 用户={sender_name}({sender_id}), 租户={tenant_id}")
# 调用实时对话接口处理消息 # 调用实时对话接口处理消息
response_data = chat_manager.process_message( response_data = chat_manager.process_message(
@@ -198,7 +215,7 @@ class FeishuLongConnService:
invocation_method=f"feishu_longconn({chat_type})" invocation_method=f"feishu_longconn({chat_type})"
) )
logger.info(f"📊 处理结果: {response_data.get('success')}") logger.info(f" 处理结果: {response_data.get('success')}")
# 提取回复 # 提取回复
if response_data.get("success"): if response_data.get("success"):
@@ -206,7 +223,7 @@ class FeishuLongConnService:
else: else:
error_msg = response_data.get('error', '未知错误') error_msg = response_data.get('error', '未知错误')
reply_text = f"处理消息时出错: {error_msg}" reply_text = f"处理消息时出错: {error_msg}"
logger.error(f"处理失败: {error_msg}") logger.error(f"处理失败: {error_msg}")
# 确保回复是字符串 # 确保回复是字符串
if isinstance(reply_text, dict): if isinstance(reply_text, dict):
@@ -220,10 +237,10 @@ class FeishuLongConnService:
# 发送回复 # 发送回复
self._reply_message(message_id, reply_text) self._reply_message(message_id, reply_text)
logger.info(" 消息处理完成") logger.info(" 消息处理完成")
except Exception as e: except Exception as e:
logger.error(f"处理消息时发生错误: {e}", exc_info=True) logger.error(f"处理消息时发生错误: {e}", exc_info=True)
# 尝试发送错误提示 # 尝试发送错误提示
try: try:
if 'message_id' in locals(): if 'message_id' in locals():
@@ -261,14 +278,14 @@ class FeishuLongConnService:
response = self.client.im.v1.message.reply(request) response = self.client.im.v1.message.reply(request)
if not response.success(): if not response.success():
logger.error(f"回复失败: {response.code} - {response.msg}") logger.error(f"回复失败: {response.code} - {response.msg}")
return False return False
logger.info(f" 回复成功: {message_id}") logger.info(f" 回复成功: {message_id}")
return True return True
except Exception as e: except Exception as e:
logger.error(f"回复消息时发生错误: {e}", exc_info=True) logger.error(f"回复消息时发生错误: {e}", exc_info=True)
return False return False
def start(self): def start(self):
@@ -300,7 +317,7 @@ class FeishuLongConnService:
logger.info("") logger.info("")
logger.info("⏹️ 用户中断,停止飞书长连接客户端") logger.info("⏹️ 用户中断,停止飞书长连接客户端")
except Exception as e: except Exception as e:
logger.error(f"飞书长连接客户端异常: {e}", exc_info=True) logger.error(f"飞书长连接客户端异常: {e}", exc_info=True)
raise raise

View File

@@ -251,11 +251,11 @@ class FeishuPermissionChecker:
result = self.check_permissions() result = self.check_permissions()
summary = "飞书权限检查结果:\n" summary = "飞书权限检查结果:\n"
summary += f"整体状态: {' 正常' if result['success'] else '异常'}\n\n" summary += f"整体状态: {' 正常' if result['success'] else '异常'}\n\n"
summary += "检查项目:\n" summary += "检查项目:\n"
for check_name, check_result in result["checks"].items(): for check_name, check_result in result["checks"].items():
status_icon = "" if check_result["status"] == "success" else "⚠️" if check_result["status"] == "warning" else "" status_icon = "" if check_result["status"] == "success" else "" if check_result["status"] == "warning" else ""
summary += f" {status_icon} {check_name}: {check_result['message']}\n" summary += f" {status_icon} {check_name}: {check_result['message']}\n"
if result["recommendations"]: if result["recommendations"]:

View File

@@ -90,7 +90,7 @@ def check_login():
'/login', '/login',
'/api/auth/login', '/api/auth/login',
'/api/auth/status', '/api/auth/status',
'/api/feishu/bot/event', # 飞书机器人回调 '/api/feishu/bot/event', # 飞书机器人回调
'/static/', '/static/',
'/uploads/' '/uploads/'
] ]

View File

@@ -187,7 +187,7 @@ def resolve_tenant_by_chat_id(chat_id: str) -> str:
return t.tenant_id return t.tenant_id
except Exception as e: except Exception as e:
logger.error(f"解析飞书群租户映射失败: {e}") logger.error(f"解析飞书群租户映射失败: {e}")
logger.warning(f"⚠️ 飞书群 {chat_id} 未绑定任何租户,使用默认租户。请在租户管理页面将此 chat_id 绑定到对应租户。") logger.warning(f" 飞书群 {chat_id} 未绑定任何租户,使用默认租户。请在租户管理页面将此 chat_id 绑定到对应租户。")
return DEFAULT_TENANT return DEFAULT_TENANT

View File

@@ -56,7 +56,7 @@ def start_feishu_longconn_service():
service = get_feishu_longconn_service() service = get_feishu_longconn_service()
service.start() # 这会阻塞当前线程 service.start() # 这会阻塞当前线程
except Exception as e: except Exception as e:
print(f"飞书长连接服务启动失败: {e}") print(f"飞书长连接服务启动失败: {e}")
import traceback import traceback
traceback.print_exc() traceback.print_exc()

View File

@@ -47,7 +47,7 @@ def main():
logger.info(f" - 日志级别: {log_level}") logger.info(f" - 日志级别: {log_level}")
logger.info("") logger.info("")
logger.info("🔌 启动模式: 事件订阅 2.0(长连接)") logger.info("🔌 启动模式: 事件订阅 2.0(长连接)")
logger.info(" 优势:") logger.info(" 优势:")
logger.info(" - 无需公网域名") logger.info(" - 无需公网域名")
logger.info(" - 无需配置 webhook") logger.info(" - 无需配置 webhook")
logger.info(" - 自动重连") logger.info(" - 自动重连")
@@ -68,7 +68,7 @@ def main():
logger.info("⏹️ 用户中断,正在停止服务...") logger.info("⏹️ 用户中断,正在停止服务...")
logger.info("👋 再见!") logger.info("👋 再见!")
except Exception as e: except Exception as e:
logger.error(f"服务异常退出: {e}", exc_info=True) logger.error(f"服务异常退出: {e}", exc_info=True)
sys.exit(1) sys.exit(1)