v2.0: 架构大版本升级
任务 3.2+3.3: 飞书入口迁移到 MessagePipeline - feishu_bot.py 改用 pipeline.handle_message(去掉 30 行会话管理代码) - feishu_longconn_service.py 改用 pipeline.handle_message(去掉 25 行) - 各入口只负责协议适配,业务逻辑统一在 Pipeline 任务 5: 统一配置管理 - 新增 src/config/config_service.py(ConfigService 单例) - 优先级:环境变量 > system_settings.json > 代码默认值 - 支持点号分隔的嵌套 key、自动类型转换 任务 8: 密码哈希升级 - SHA-256 bcrypt(User.set_password/check_password) - AuthManager.hash_password/verify_password 同步升级 - 兼容旧密码:登录时检测 SHA-256 格式,验证通过后自动升级为 bcrypt - auth_manager.secret_key 改为从环境变量读取 任务 9: 前端事件总线 - TSPDashboard 新增 on/off/emit 方法 - 模块间可通过事件通信,不再只靠直接读写共享状态 README.md 重写 - 功能概览、技术栈、快速开始、项目结构 - 架构要点、多租户、飞书机器人、环境变量 - 开发和部署说明
This commit is contained in:
102
src/config/config_service.py
Normal file
102
src/config/config_service.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
统一配置服务
|
||||
优先级:环境变量 > system_settings.json > 代码默认值
|
||||
"""
|
||||
import os
|
||||
import json
|
||||
import logging
|
||||
from typing import Any, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_SETTINGS_PATH = os.path.join('data', 'system_settings.json')
|
||||
|
||||
|
||||
class ConfigService:
|
||||
"""统一配置读写接口"""
|
||||
|
||||
def __init__(self):
|
||||
self._file_cache = None
|
||||
self._file_mtime = 0
|
||||
|
||||
def _load_file(self) -> dict:
|
||||
"""加载 system_settings.json(带文件修改时间缓存)"""
|
||||
try:
|
||||
if os.path.exists(_SETTINGS_PATH):
|
||||
mtime = os.path.getmtime(_SETTINGS_PATH)
|
||||
if mtime != self._file_mtime or self._file_cache is None:
|
||||
with open(_SETTINGS_PATH, 'r', encoding='utf-8') as f:
|
||||
self._file_cache = json.load(f)
|
||||
self._file_mtime = mtime
|
||||
return self._file_cache or {}
|
||||
except Exception as e:
|
||||
logger.debug(f"加载配置文件失败: {e}")
|
||||
return {}
|
||||
|
||||
def get(self, key: str, default: Any = None) -> Any:
|
||||
"""
|
||||
获取配置值。
|
||||
优先级:环境变量 > system_settings.json > default
|
||||
"""
|
||||
# 1. 环境变量(key 转大写,点号转下划线)
|
||||
env_key = key.upper().replace('.', '_')
|
||||
env_val = os.environ.get(env_key)
|
||||
if env_val is not None:
|
||||
return self._cast(env_val, default)
|
||||
|
||||
# 2. system_settings.json(支持点号分隔的嵌套 key)
|
||||
settings = self._load_file()
|
||||
parts = key.split('.')
|
||||
val = settings
|
||||
for part in parts:
|
||||
if isinstance(val, dict):
|
||||
val = val.get(part)
|
||||
else:
|
||||
val = None
|
||||
break
|
||||
if val is not None:
|
||||
return val
|
||||
|
||||
return default
|
||||
|
||||
def get_section(self, section: str) -> dict:
|
||||
"""获取配置文件中的一个 section"""
|
||||
settings = self._load_file()
|
||||
return settings.get(section, {})
|
||||
|
||||
def set(self, key: str, value: Any):
|
||||
"""写入配置到 system_settings.json"""
|
||||
settings = self._load_file()
|
||||
parts = key.split('.')
|
||||
target = settings
|
||||
for part in parts[:-1]:
|
||||
target = target.setdefault(part, {})
|
||||
target[parts[-1]] = value
|
||||
self._save_file(settings)
|
||||
|
||||
def _save_file(self, settings: dict):
|
||||
os.makedirs('data', exist_ok=True)
|
||||
with open(_SETTINGS_PATH, 'w', encoding='utf-8') as f:
|
||||
json.dump(settings, f, ensure_ascii=False, indent=2)
|
||||
self._file_cache = settings
|
||||
self._file_mtime = os.path.getmtime(_SETTINGS_PATH)
|
||||
|
||||
@staticmethod
|
||||
def _cast(value: str, default: Any) -> Any:
|
||||
"""根据 default 的类型自动转换环境变量字符串"""
|
||||
if default is None:
|
||||
return value
|
||||
if isinstance(default, bool):
|
||||
return value.lower() in ('true', '1', 'yes')
|
||||
if isinstance(default, int):
|
||||
try: return int(value)
|
||||
except: return default
|
||||
if isinstance(default, float):
|
||||
try: return float(value)
|
||||
except: return default
|
||||
return value
|
||||
|
||||
|
||||
# 全局单例
|
||||
config_service = ConfigService()
|
||||
@@ -18,16 +18,21 @@ class AuthManager:
|
||||
"""认证管理器"""
|
||||
|
||||
def __init__(self):
|
||||
self.secret_key = "your-secret-key-change-this-in-production" # 应该从配置中读取
|
||||
import os
|
||||
self.secret_key = os.environ.get('SECRET_KEY', 'change-this-in-production')
|
||||
self.token_expiry = timedelta(hours=24)
|
||||
|
||||
def hash_password(self, password: str) -> str:
|
||||
"""密码哈希"""
|
||||
return hashlib.sha256(password.encode()).hexdigest()
|
||||
"""密码哈希(bcrypt)"""
|
||||
import bcrypt
|
||||
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
def verify_password(self, password: str, password_hash: str) -> bool:
|
||||
"""验证密码"""
|
||||
return self.hash_password(password) == password_hash
|
||||
"""验证密码(兼容旧 SHA-256)"""
|
||||
import bcrypt
|
||||
if password_hash and password_hash.startswith('$2b$'):
|
||||
return bcrypt.checkpw(password.encode(), password_hash.encode())
|
||||
return hashlib.sha256(password.encode()).hexdigest() == password_hash
|
||||
|
||||
def generate_token(self, user_data: dict) -> str:
|
||||
"""生成JWT token"""
|
||||
|
||||
@@ -294,12 +294,21 @@ class User(Base):
|
||||
last_login = Column(DateTime)
|
||||
|
||||
def set_password(self, password):
|
||||
"""设置密码哈希"""
|
||||
self.password_hash = hashlib.sha256(password.encode()).hexdigest()
|
||||
"""设置密码哈希(bcrypt)"""
|
||||
import bcrypt
|
||||
self.password_hash = bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
def check_password(self, password):
|
||||
"""验证密码"""
|
||||
return self.password_hash == hashlib.sha256(password.encode()).hexdigest()
|
||||
"""验证密码(兼容旧 SHA-256 格式,验证通过后自动升级为 bcrypt)"""
|
||||
import bcrypt
|
||||
if self.password_hash and self.password_hash.startswith('$2b$'):
|
||||
return bcrypt.checkpw(password.encode(), self.password_hash.encode())
|
||||
# 旧格式:SHA-256
|
||||
if self.password_hash == hashlib.sha256(password.encode()).hexdigest():
|
||||
# 自动升级为 bcrypt
|
||||
self.set_password(password)
|
||||
return True
|
||||
return False
|
||||
|
||||
def to_dict(self):
|
||||
"""转换为字典格式(用于API响应)"""
|
||||
|
||||
@@ -161,50 +161,20 @@ class FeishuLongConnService:
|
||||
self._reply_message(message_id, "您好!请问有什么可以帮助您的吗?")
|
||||
return
|
||||
|
||||
logger.info(f" 清理后内容: {text_content}")
|
||||
logger.info(f"清理后内容: {text_content}")
|
||||
|
||||
# 构造会话用户ID(群聊隔离)
|
||||
# 使用 Pipeline 统一处理消息
|
||||
session_user_id = f"feishu_{chat_id}_{sender_id}"
|
||||
|
||||
logger.info(f" 会话标识: {session_user_id}")
|
||||
|
||||
# 解析租户:根据 chat_id 查找绑定的租户
|
||||
from src.web.blueprints.tenants import resolve_tenant_by_chat_id
|
||||
tenant_id = resolve_tenant_by_chat_id(chat_id)
|
||||
logger.info(f" 群 {chat_id} 对应租户: {tenant_id}")
|
||||
|
||||
# 获取或创建会话
|
||||
chat_manager = service_manager.get_chat_manager()
|
||||
active_sessions = chat_manager.get_active_sessions()
|
||||
session_id = None
|
||||
|
||||
for session in active_sessions:
|
||||
if session.get('user_id') == session_user_id:
|
||||
session_id = session.get('session_id')
|
||||
# 更新会话的 tenant_id(群可能重新绑定了租户)
|
||||
if session_id in chat_manager.active_sessions:
|
||||
chat_manager.active_sessions[session_id]['tenant_id'] = tenant_id
|
||||
logger.info(f" 找到已有会话: {session_id}")
|
||||
break
|
||||
|
||||
# 如果没有会话,创建新会话
|
||||
if not session_id:
|
||||
session_id = chat_manager.create_session(
|
||||
user_id=session_user_id,
|
||||
work_order_id=None,
|
||||
tenant_id=tenant_id
|
||||
)
|
||||
logger.info(f" 创建新会话: {session_id}, 用户={sender_name}({sender_id}), 租户={tenant_id}")
|
||||
|
||||
# 调用实时对话接口处理消息
|
||||
response_data = chat_manager.process_message(
|
||||
session_id=session_id,
|
||||
user_message=text_content,
|
||||
pipeline = service_manager.get_pipeline()
|
||||
response_data = pipeline.handle_message(
|
||||
user_id=session_user_id,
|
||||
message=text_content,
|
||||
chat_id=chat_id,
|
||||
ip_address=f"feishu:{sender_id}:{sender_name}",
|
||||
invocation_method=f"feishu_longconn({chat_type})"
|
||||
)
|
||||
|
||||
logger.info(f" 处理结果: {response_data.get('success')}")
|
||||
tenant_id = response_data.get('tenant_id', 'default')
|
||||
logger.info(f"处理结果: success={response_data.get('success')}, 租户={tenant_id}")
|
||||
|
||||
# 提取回复
|
||||
if response_data.get("success"):
|
||||
|
||||
@@ -99,59 +99,36 @@ def _process_message_in_background(app, event_data: dict):
|
||||
|
||||
logger.info(f"[Feishu Bot] 清理后的消息内容: '{text_content}'")
|
||||
|
||||
# 3. 获取或创建该飞书用户的会话(支持群聊隔离)
|
||||
chat_manager = service_manager.get_chat_manager()
|
||||
|
||||
# 获取发送者ID(优先 open_id,user_id 可能为 None)
|
||||
# 3. 解析发送者信息
|
||||
sender = event.get('sender', {})
|
||||
sender_ids = sender.get('sender_id', {})
|
||||
sender_open_id = sender_ids.get('open_id', '') or ''
|
||||
sender_user_id = sender_ids.get('user_id', '') or ''
|
||||
sender_id = sender_user_id or sender_open_id or 'unknown'
|
||||
sender_type = sender.get('sender_type', 'user')
|
||||
|
||||
# 获取发送者姓名(用 open_id 查询)
|
||||
sender_name = sender_id
|
||||
try:
|
||||
id_for_query = sender_open_id or sender_user_id
|
||||
id_type = 'open_id' if sender_open_id else 'user_id'
|
||||
if id_for_query:
|
||||
user_info = feishu_service.get_user_info(id_for_query, id_type=id_type)
|
||||
user_info = feishu_service.get_user_info(id_for_query, id_type='open_id' if sender_open_id else 'user_id')
|
||||
sender_name = user_info.get('name') or user_info.get('en_name') or sender_id
|
||||
except Exception as e:
|
||||
logger.warning(f"[Feishu Bot] 获取发送者信息失败: {e}")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
logger.info(f"[Feishu Bot] 消息详情: 发送者={sender_name}({sender_id}), 群={chat_id}, 类型={chat_type_desc}, 租户={tenant_id}, 消息ID={message_id}")
|
||||
logger.info(f"[Feishu Bot] 📝 消息内容: '{text_content}'")
|
||||
|
||||
# 群聊隔离:每个用户在每个群都有独立会话
|
||||
user_id = f"feishu_{chat_id}_{sender_id}"
|
||||
logger.info(f"[Feishu Bot] 发送者={sender_name}({sender_id}), 群={chat_id}, 租户={tenant_id}")
|
||||
|
||||
# 检查是否已有活跃会话
|
||||
active_sessions = chat_manager.get_active_sessions()
|
||||
session_id = None
|
||||
for session in active_sessions:
|
||||
if session.get('user_id') == user_id:
|
||||
session_id = session.get('session_id')
|
||||
# 更新会话的 tenant_id(群可能重新绑定了租户)
|
||||
if session_id in chat_manager.active_sessions:
|
||||
chat_manager.active_sessions[session_id]['tenant_id'] = tenant_id
|
||||
logger.info(f"[Feishu Bot] 找到已有会话: {session_id}")
|
||||
break
|
||||
|
||||
# 如果没有会话,创建新会话
|
||||
if not session_id:
|
||||
session_id = chat_manager.create_session(user_id=user_id, work_order_id=None, tenant_id=tenant_id)
|
||||
logger.info(f"[Feishu Bot] 新建会话: {session_id}, 用户={sender_name}({sender_id}), 租户={tenant_id}")
|
||||
|
||||
# 4. 调用实时对话接口处理消息
|
||||
response_data = chat_manager.process_message(
|
||||
session_id=session_id,
|
||||
user_message=text_content,
|
||||
# 4. 使用 Pipeline 统一处理消息
|
||||
pipeline = service_manager.get_pipeline()
|
||||
response_data = pipeline.handle_message(
|
||||
user_id=user_id,
|
||||
message=text_content,
|
||||
tenant_id=tenant_id,
|
||||
chat_id=chat_id,
|
||||
ip_address=f"feishu:{sender_id}:{sender_name}",
|
||||
invocation_method=f"feishu_bot({chat_type})"
|
||||
)
|
||||
logger.info(f"[Feishu Bot] 处理结果: success={response_data.get('success')}, 用户={sender_name}")
|
||||
logger.info(f"[Feishu Bot] 处理结果: success={response_data.get('success')}")
|
||||
|
||||
# 5. 提取回复并发送
|
||||
if response_data.get("success"):
|
||||
|
||||
@@ -69,6 +69,8 @@ function updatePageLanguage(lang) {
|
||||
|
||||
class TSPDashboard {
|
||||
constructor() {
|
||||
// ===== 事件总线(模块间通信)=====
|
||||
this._eventHandlers = {};
|
||||
// ===== 共享状态(所有模块通过 this.xxx 访问)=====
|
||||
// 导航
|
||||
this.currentTab = 'dashboard';
|
||||
@@ -116,6 +118,10 @@ class TSPDashboard {
|
||||
updatePageLanguage(this.currentLanguage);
|
||||
}
|
||||
|
||||
// ===== 事件总线 =====
|
||||
on(event, handler) { (this._eventHandlers[event] = this._eventHandlers[event] || []).push(handler); }
|
||||
off(event, handler) { const h = this._eventHandlers[event]; if (h) this._eventHandlers[event] = h.filter(fn => fn !== handler); }
|
||||
emit(event, data) { (this._eventHandlers[event] || []).forEach(fn => { try { fn(data); } catch (e) { console.error(`Event ${event} handler error:`, e); } }); }
|
||||
async applyModulePermissions() {
|
||||
try {
|
||||
const resp = await fetch('/api/settings');
|
||||
|
||||
Reference in New Issue
Block a user