1. 新增 Tenant 模型(tenants 表),支持租户创建、重命名、删除 2. 新增 /api/tenants CRUD 蓝图,default 租户不可删除 3. 数据库初始化时自动创建默认租户记录 4. Dashboard 新增租户管理标签页(创建/编辑/删除租户) 5. 各业务模块写入数据时正确传递 tenant_id: - realtime_chat: create_session 和 _save_conversation 支持 tenant_id - dialogue_manager: _save_conversation 和 create_work_order 支持 tenant_id - conversation_history: save_conversation 支持 tenant_id - workorder_sync: sync_from_feishu 支持 tenant_id - websocket_server: create_session 传递 tenant_id - HTTP chat API: create_session 传递 tenant_id - feishu_sync API: 同步时传递 tenant_id - workorders API: 创建工单时传递 tenant_id 6. 网页对话入口添加租户选择器 7. 知识库搜索按租户隔离(realtime_chat 中 _search_knowledge 传递 tenant_id) 8. 初始化时自动加载租户列表填充选择器
447 lines
15 KiB
Python
447 lines
15 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
飞书同步蓝图
|
||
处理飞书多维表格与工单系统的同步
|
||
"""
|
||
|
||
from flask import Blueprint, request, jsonify, render_template
|
||
from src.integrations.feishu_client import FeishuClient
|
||
from src.integrations.workorder_sync import WorkOrderSyncService
|
||
from src.integrations.config_manager import config_manager
|
||
from src.integrations.feishu_permission_checker import FeishuPermissionChecker
|
||
import logging
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
feishu_sync_bp = Blueprint('feishu_sync', __name__, url_prefix='/api/feishu-sync')
|
||
|
||
# 全局同步服务实例
|
||
sync_service = None
|
||
|
||
def get_sync_service():
|
||
"""获取同步服务实例"""
|
||
global sync_service
|
||
if sync_service is None:
|
||
# 从配置管理器读取飞书配置
|
||
feishu_config = config_manager.get_feishu_config()
|
||
|
||
if not all([feishu_config.get("app_id"), feishu_config.get("app_secret"),
|
||
feishu_config.get("app_token"), feishu_config.get("table_id")]):
|
||
raise Exception("飞书配置不完整,请先配置飞书应用信息")
|
||
|
||
feishu_client = FeishuClient(feishu_config["app_id"], feishu_config["app_secret"])
|
||
sync_service = WorkOrderSyncService(feishu_client, feishu_config["app_token"], feishu_config["table_id"])
|
||
|
||
return sync_service
|
||
|
||
@feishu_sync_bp.route('/config', methods=['GET', 'POST'])
|
||
def manage_config():
|
||
"""管理飞书同步配置"""
|
||
if request.method == 'GET':
|
||
# 返回当前配置
|
||
try:
|
||
config_summary = config_manager.get_config_summary()
|
||
return jsonify({
|
||
"success": True,
|
||
"config": config_summary
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"获取配置失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
elif request.method == 'POST':
|
||
# 更新配置
|
||
try:
|
||
data = request.get_json()
|
||
app_id = data.get('app_id')
|
||
app_secret = data.get('app_secret')
|
||
app_token = data.get('app_token')
|
||
table_id = data.get('table_id')
|
||
|
||
if not all([app_id, app_secret, app_token, table_id]):
|
||
return jsonify({"error": "缺少必要配置参数"}), 400
|
||
|
||
# 更新配置管理器
|
||
success = config_manager.update_feishu_config(
|
||
app_id=app_id,
|
||
app_secret=app_secret,
|
||
app_token=app_token,
|
||
table_id=table_id
|
||
)
|
||
|
||
if success:
|
||
# 重新初始化同步服务
|
||
global sync_service
|
||
sync_service = None # 强制重新创建
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "配置更新成功"
|
||
})
|
||
else:
|
||
return jsonify({"error": "配置更新失败"}), 500
|
||
|
||
except Exception as e:
|
||
logger.error(f"更新飞书配置失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/sync-from-feishu', methods=['POST'])
|
||
def sync_from_feishu():
|
||
"""从飞书同步数据到本地"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
generate_ai = data.get('generate_ai_suggestions', True)
|
||
limit = data.get('limit', 10)
|
||
tenant_id = data.get('tenant_id')
|
||
|
||
sync_service = get_sync_service()
|
||
result = sync_service.sync_from_feishu(generate_ai_suggestions=generate_ai, limit=limit, tenant_id=tenant_id)
|
||
|
||
if result.get("success"):
|
||
message = f"同步完成:创建 {result['created_count']} 条,更新 {result['updated_count']} 条"
|
||
if result.get('ai_suggestions_generated'):
|
||
message += ",AI建议已生成并更新到飞书表格"
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": message,
|
||
"details": result
|
||
})
|
||
else:
|
||
return jsonify({"error": result.get("error")}), 500
|
||
|
||
except Exception as e:
|
||
logger.error(f"从飞书同步失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/sync-to-feishu/<int:workorder_id>', methods=['POST'])
|
||
def sync_to_feishu(workorder_id):
|
||
"""将本地工单同步到飞书"""
|
||
try:
|
||
sync_service = get_sync_service()
|
||
result = sync_service.sync_to_feishu(workorder_id)
|
||
|
||
if result.get("success"):
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "同步到飞书成功"
|
||
})
|
||
else:
|
||
return jsonify({"error": result.get("error")}), 500
|
||
|
||
except Exception as e:
|
||
logger.error(f"同步到飞书失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/status')
|
||
def get_sync_status():
|
||
"""获取同步状态"""
|
||
try:
|
||
sync_service = get_sync_service()
|
||
status = sync_service.get_sync_status()
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"status": status
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"获取同步状态失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/test-connection')
|
||
def test_connection():
|
||
"""测试飞书连接"""
|
||
try:
|
||
# 使用配置管理器测试连接
|
||
result = config_manager.test_feishu_connection()
|
||
|
||
if result.get("success"):
|
||
# 如果连接成功,尝试获取表格字段信息
|
||
try:
|
||
sync_service = get_sync_service()
|
||
|
||
# 使用新的测试连接方法
|
||
connection_test = sync_service.feishu_client.test_connection()
|
||
if not connection_test.get("success"):
|
||
return jsonify({
|
||
"success": False,
|
||
"message": f"飞书连接测试失败: {connection_test.get('message')}"
|
||
}), 400
|
||
|
||
fields_info = sync_service.feishu_client.get_table_fields(
|
||
sync_service.app_token, sync_service.table_id
|
||
)
|
||
|
||
if fields_info.get("code") == 0:
|
||
result["fields"] = fields_info.get("data", {}).get("items", [])
|
||
except Exception as e:
|
||
logger.warning(f"获取表格字段信息失败: {e}")
|
||
|
||
return jsonify(result)
|
||
|
||
except Exception as e:
|
||
logger.error(f"测试飞书连接失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/create-workorder', methods=['POST'])
|
||
def create_workorder_from_feishu():
|
||
"""从飞书记录创建工单"""
|
||
try:
|
||
data = request.get_json()
|
||
record_id = data.get('record_id')
|
||
|
||
if not record_id:
|
||
return jsonify({"success": False, "message": "缺少记录ID"}), 400
|
||
|
||
sync_service = get_sync_service()
|
||
result = sync_service.create_workorder_from_feishu_record(record_id)
|
||
|
||
if result.get("success"):
|
||
return jsonify(result)
|
||
else:
|
||
return jsonify(result), 400
|
||
|
||
except Exception as e:
|
||
logger.error(f"创建工单失败: {e}")
|
||
return jsonify({"success": False, "message": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/field-mapping/status')
|
||
def get_field_mapping_status():
|
||
"""获取字段映射状态"""
|
||
try:
|
||
sync_service = get_sync_service()
|
||
status = sync_service.get_mapping_status()
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"status": status
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"获取字段映射状态失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/field-mapping/discover', methods=['POST'])
|
||
def discover_fields():
|
||
"""发现字段并生成映射建议"""
|
||
try:
|
||
data = request.get_json() or {}
|
||
limit = data.get('limit', 5) # 默认分析5条记录
|
||
|
||
sync_service = get_sync_service()
|
||
|
||
# 获取飞书记录进行分析
|
||
feishu_client = sync_service.feishu_client
|
||
records = feishu_client.get_table_records(sync_service.app_token, sync_service.table_id, page_size=limit)
|
||
|
||
if records.get("code") != 0:
|
||
raise Exception(f"获取飞书记录失败: {records.get('msg', '未知错误')}")
|
||
|
||
items = records.get("data", {}).get("items", [])
|
||
if not items:
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "没有找到飞书记录",
|
||
"discovery_report": {}
|
||
})
|
||
|
||
# 分析第一条记录的字段
|
||
first_record = items[0]
|
||
feishu_fields = feishu_client.parse_record_fields(first_record)
|
||
|
||
# 生成字段发现报告
|
||
discovery_report = sync_service.get_field_discovery_report(feishu_fields)
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"discovery_report": discovery_report,
|
||
"sample_record": feishu_fields
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"字段发现失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/field-mapping/add', methods=['POST'])
|
||
def add_field_mapping():
|
||
"""添加字段映射"""
|
||
try:
|
||
data = request.get_json()
|
||
feishu_field = data.get('feishu_field')
|
||
local_field = data.get('local_field')
|
||
aliases = data.get('aliases', [])
|
||
patterns = data.get('patterns', [])
|
||
priority = data.get('priority', 3)
|
||
|
||
if not feishu_field or not local_field:
|
||
return jsonify({"error": "缺少必要参数"}), 400
|
||
|
||
sync_service = get_sync_service()
|
||
success = sync_service.add_field_mapping(
|
||
feishu_field=feishu_field,
|
||
local_field=local_field,
|
||
aliases=aliases,
|
||
patterns=patterns,
|
||
priority=priority
|
||
)
|
||
|
||
if success:
|
||
return jsonify({
|
||
"success": True,
|
||
"message": f"字段映射 '{feishu_field}' -> '{local_field}' 添加成功"
|
||
})
|
||
else:
|
||
return jsonify({"error": "添加字段映射失败"}), 500
|
||
|
||
except Exception as e:
|
||
logger.error(f"添加字段映射失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/field-mapping/remove', methods=['POST'])
|
||
def remove_field_mapping():
|
||
"""移除字段映射"""
|
||
try:
|
||
data = request.get_json()
|
||
feishu_field = data.get('feishu_field')
|
||
|
||
if not feishu_field:
|
||
return jsonify({"error": "缺少字段名参数"}), 400
|
||
|
||
sync_service = get_sync_service()
|
||
success = sync_service.remove_field_mapping(feishu_field)
|
||
|
||
if success:
|
||
return jsonify({
|
||
"success": True,
|
||
"message": f"字段映射 '{feishu_field}' 移除成功"
|
||
})
|
||
else:
|
||
return jsonify({
|
||
"success": False,
|
||
"message": f"字段映射 '{feishu_field}' 不存在或移除失败"
|
||
})
|
||
|
||
except Exception as e:
|
||
logger.error(f"移除字段映射失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/check-permissions')
|
||
def check_permissions():
|
||
"""检查飞书权限"""
|
||
try:
|
||
checker = FeishuPermissionChecker()
|
||
result = checker.check_permissions()
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"permission_check": result,
|
||
"summary": checker.get_permission_summary()
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"权限检查失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/field-mapping')
|
||
def field_mapping_page():
|
||
"""字段映射管理页面"""
|
||
return render_template('field_mapping.html')
|
||
|
||
@feishu_sync_bp.route('/preview-feishu-data')
|
||
def preview_feishu_data():
|
||
"""预览飞书数据"""
|
||
try:
|
||
sync_service = get_sync_service()
|
||
|
||
# 获取前10条记录进行预览
|
||
records = sync_service.feishu_client.get_table_records(
|
||
sync_service.app_token, sync_service.table_id, page_size=10
|
||
)
|
||
|
||
if records.get("code") == 0:
|
||
items = records.get("data", {}).get("items", [])
|
||
preview_data = []
|
||
|
||
for record in items:
|
||
parsed_fields = sync_service.feishu_client.parse_record_fields(record)
|
||
preview_data.append({
|
||
"record_id": record.get("record_id"),
|
||
"fields": parsed_fields
|
||
})
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"preview_data": preview_data,
|
||
"total_count": len(preview_data)
|
||
})
|
||
else:
|
||
return jsonify({
|
||
"success": False,
|
||
"error": records.get("msg", "获取数据失败")
|
||
}), 500
|
||
|
||
except Exception as e:
|
||
logger.error(f"预览飞书数据失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/config/export', methods=['GET'])
|
||
def export_config():
|
||
"""导出配置"""
|
||
try:
|
||
config_json = config_manager.export_config()
|
||
return jsonify({
|
||
"success": True,
|
||
"config": config_json
|
||
})
|
||
except Exception as e:
|
||
logger.error(f"导出配置失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/config/import', methods=['POST'])
|
||
def import_config():
|
||
"""导入配置"""
|
||
try:
|
||
data = request.get_json()
|
||
config_json = data.get('config')
|
||
|
||
if not config_json:
|
||
return jsonify({"error": "缺少配置数据"}), 400
|
||
|
||
success = config_manager.import_config(config_json)
|
||
|
||
if success:
|
||
# 重新初始化同步服务
|
||
global sync_service
|
||
sync_service = None
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "配置导入成功"
|
||
})
|
||
else:
|
||
return jsonify({"error": "配置导入失败"}), 500
|
||
|
||
except Exception as e:
|
||
logger.error(f"导入配置失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|
||
|
||
@feishu_sync_bp.route('/config/reset', methods=['POST'])
|
||
def reset_config():
|
||
"""重置配置"""
|
||
try:
|
||
success = config_manager.reset_config()
|
||
|
||
if success:
|
||
# 重新初始化同步服务
|
||
global sync_service
|
||
sync_service = None
|
||
|
||
return jsonify({
|
||
"success": True,
|
||
"message": "配置重置成功"
|
||
})
|
||
else:
|
||
return jsonify({"error": "配置重置失败"}), 500
|
||
|
||
except Exception as e:
|
||
logger.error(f"重置配置失败: {e}")
|
||
return jsonify({"error": str(e)}), 500
|