import asyncio import threading from concurrent.futures import ThreadPoolExecutor from flask import Blueprint, render_template, jsonify, request, current_app from .auth import login_required from loguru import logger # 创建联系人管理蓝图 contacts_bp = Blueprint('contacts', __name__, url_prefix='/contacts') # 创建线程池 message_thread_pool = ThreadPoolExecutor(max_workers=10, thread_name_prefix="message_sender_") # 创建共享的事件循环 shared_loop = None loop_lock = threading.Lock() def get_or_create_loop(): """获取或创建共享的事件循环""" global shared_loop with loop_lock: if shared_loop is None: shared_loop = asyncio.new_event_loop() # 在新线程中运行事件循环 def run_loop(): asyncio.set_event_loop(shared_loop) shared_loop.run_forever() loop_thread = threading.Thread(target=run_loop, daemon=True) loop_thread.start() return shared_loop def send_message_in_thread(func, *args, **kwargs): """使用共享事件循环发送消息""" def run(): try: loop = get_or_create_loop() # 创建异步任务 async def send(): try: await func(*args, **kwargs) except Exception as e: logger.error(f"发送消息失败: {e}") # 在共享事件循环中运行任务 future = asyncio.run_coroutine_threadsafe(send(), loop) # 等待任务完成,设置超时时间 future.result(timeout=30) except Exception as e: logger.error(f"消息发送任务执行失败: {e}") # 使用线程池提交任务 message_thread_pool.submit(run) def run_member_context_refresh_in_thread(func, *args, **kwargs): """在线程池中异步刷新成员交互摘要,避免阻塞请求线程""" def run(): try: func(*args, **kwargs) except Exception as e: logger.error(f"成员交互摘要后台刷新失败: {e}") message_thread_pool.submit(run) # 联系人管理页面 @contacts_bp.route('/') @login_required def contacts_management(): """通讯录管理页面""" return render_template('contacts_management.html') # API路由 @contacts_bp.route('/api/all', methods=['GET']) @login_required def api_contacts_all(): """获取所有联系人信息API""" try: server = current_app.dashboard_server contacts = server.contact_manager.get_contacts() return jsonify({ "success": True, "data": { "contacts": contacts } }) except Exception as e: logger.error(f"获取所有联系人信息失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/statistics', methods=['GET']) @login_required def api_contacts_statistics(): """获取联系人统计信息API""" try: server = current_app.dashboard_server # 使用新的联系人分类方法获取统计信息 total, groups, personal, public, official = server.contact_manager.get_contact_statistics() return jsonify({ "success": True, "data": { "total": total, "groups": groups, "personal": personal, "public": public, "official": official } }) except Exception as e: logger.error(f"获取联系人统计信息失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/groups', methods=['GET']) @login_required def api_contacts_groups(): """获取群组联系人信息API""" try: server = current_app.dashboard_server group_contacts = server.contact_manager.get_group_contacts() return jsonify({ "success": True, "data": { "groups": group_contacts } }) except Exception as e: logger.error(f"获取群组联系人信息失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/personal', methods=['GET']) @login_required def api_contacts_personal(): """获取个人联系人信息API""" try: server = current_app.dashboard_server personal_contacts = server.contact_manager.get_personal_contacts() return jsonify({ "success": True, "data": { "personal": personal_contacts } }) except Exception as e: logger.error(f"获取个人联系人信息失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/official', methods=['GET']) @login_required def api_contacts_official(): """获取公众号联系人信息API""" try: server = current_app.dashboard_server official_accounts = server.contact_manager.get_official_accounts() return jsonify({ "success": True, "data": { "official": official_accounts } }) except Exception as e: logger.error(f"获取公众号联系人信息失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/public', methods=['GET']) @login_required def api_contacts_public(): """获取公共好友信息API""" try: server = current_app.dashboard_server public_contacts = server.contact_manager.get_public_contacts() return jsonify({ "success": True, "data": { "public": public_contacts } }) except Exception as e: logger.error(f"获取公共好友信息失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/head_images', methods=['GET']) @login_required def api_head_images(): """获取联系人头像信息API""" try: server = current_app.dashboard_server head_images = server.contact_manager.get_all_head_images() return jsonify({ "success": True, "data": { "head_images": head_images } }) except Exception as e: logger.error(f"获取联系人头像信息失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/group_members/', methods=['GET']) @login_required def api_group_members(roomid): """获取指定群的成员列表API Args: roomid: 群ID """ try: server = current_app.dashboard_server group_members = server.contact_db.get_chatroom_small_member_list(roomid) context_enabled = bool(server.member_context_service) and server.member_context_service.is_group_enabled(roomid) if context_enabled: contexts = server.member_context_db.list_group_member_contexts(roomid) context_map = {item.get("wxid"): item for item in contexts} for member in group_members: context = context_map.get(member.get("wxid"), {}) member["activity_level"] = context.get("activity_level", "") member["response_style_hint"] = context.get("response_style_hint", "") member["summary_text"] = context.get("summary_text", "") member["last_profiled_at"] = context.get("last_profiled_at", "") else: for member in group_members: member["activity_level"] = "" member["response_style_hint"] = "" member["summary_text"] = "" member["last_profiled_at"] = "" return jsonify({ "success": True, "data": { "members": group_members, "member_context_enabled": context_enabled } }) except Exception as e: logger.error(f"获取群成员列表失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/group_member_context//', methods=['GET']) @login_required def api_group_member_context(roomid, wxid): """获取群成员交互摘要""" try: server = current_app.dashboard_server if not server.member_context_service: return jsonify({"success": False, "error": "成员交互摘要插件未加载"}), 503 if not server.member_context_service.is_group_enabled(roomid): return jsonify({"success": False, "error": "该群未启用成员交互摘要功能"}), 403 context = server.member_context_db.get_member_context(roomid, wxid) return jsonify({ "success": True, "data": { "context": context, "ready": bool(context) } }) except Exception as e: logger.error(f"获取群成员交互摘要失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/group_member_context/refresh', methods=['POST']) @login_required def api_refresh_group_member_context(): """刷新群成员交互摘要""" try: server = current_app.dashboard_server if not server.member_context_service: return jsonify({"success": False, "error": "成员交互摘要插件未加载"}), 503 data = request.json or {} roomid = data.get("roomid") wxid = data.get("wxid") if roomid and wxid: if not server.member_context_service.is_group_enabled(roomid): return jsonify({"success": False, "error": "该群未启用成员交互摘要功能"}), 403 run_member_context_refresh_in_thread(server.member_context_service.refresh_member_context, roomid, wxid) return jsonify({"success": True, "message": "成员交互摘要刷新任务已提交"}) if roomid: if not server.member_context_service.is_group_enabled(roomid): return jsonify({"success": False, "error": "该群未启用成员交互摘要功能"}), 403 run_member_context_refresh_in_thread(server.member_context_service.refresh_group_contexts, roomid) return jsonify({"success": True, "message": "本群成员交互摘要刷新任务已提交"}) run_member_context_refresh_in_thread(server.member_context_service.refresh_all_chatrooms) return jsonify({"success": True, "message": "全量成员交互摘要刷新任务已提交"}) except Exception as e: logger.error(f"刷新群成员交互摘要失败: {e}") return jsonify({"success": False, "error": str(e)}), 500 @contacts_bp.route('/api/update', methods=['POST']) @login_required def api_contacts_update(): """更新通讯录信息API""" try: server = current_app.dashboard_server # 假设 contact_manager 有 update_contacts 方法用于同步通讯录 result = asyncio.run(server.robot.refresh_contacts_db()) if result: return jsonify({"success": True, "message": "通讯录更新成功"}) else: return jsonify({"success": False, "message": "通讯录更新失败"}), 500 except Exception as e: logger.error(f"更新通讯录失败: {e}") return jsonify({"success": False, "message": f"更新通讯录失败: {str(e)}"}), 500 @contacts_bp.route('/api/send_message', methods=['POST']) @login_required def api_send_message(): """发送消息API 支持的消息类型: - text: 文本消息 - image: 图片消息 - voice: 语音消息 - video: 视频消息 - link: 链接消息 """ try: data = request.form if request.files else request.json wxid = data.get('wxid') msg_type = data.get('type') content = data.get('content') if not wxid or not msg_type: return jsonify({'success': False, 'message': '缺少必要参数'}) # 获取机器人实例 server = current_app.dashboard_server if not server or not server.client: return jsonify({'success': False, 'message': '机器人未初始化'}) # 根据消息类型发送消息 if msg_type == 'text': send_message_in_thread(server.client.send_text_message, wxid, content) return jsonify({ 'success': True, 'message': '消息发送中' }) elif msg_type == 'image': if 'file' not in request.files: return jsonify({'success': False, 'message': '未上传文件'}) file = request.files['file'] send_message_in_thread(server.client.send_image_message, wxid, file.read()) return jsonify({ 'success': True, 'message': '消息发送中' }) elif msg_type == 'voice': if 'file' not in request.files: return jsonify({'success': False, 'message': '未上传文件'}) file = request.files['file'] if file.filename.endswith('.mp3'): format_str = "mp3" elif file.filename.endswith('.wav'): format_str = "wav" else: return jsonify({ 'success': False, 'message': '不支持的音频格式' }) send_message_in_thread(server.client.send_voice_message, wxid, file.read(), format=format_str) return jsonify({ 'success': True, 'message': '消息发送中' }) elif msg_type == 'video': if 'file' not in request.files: return jsonify({'success': False, 'message': '未上传文件'}) file = request.files['file'] send_message_in_thread(server.client.send_video_message, wxid, file.read()) return jsonify({ 'success': True, 'message': '消息发送中' }) elif msg_type == 'link': url = content.get('url') title = content.get('title', '') description = content.get('description', '') send_message_in_thread(server.client.send_link_message, wxid, url, title, description) return jsonify({ 'success': True, 'message': '消息发送中' }) else: return jsonify({'success': False, 'message': '不支持的消息类型'}) except Exception as e: logger.exception(f"发送消息失败: {e}") return jsonify({'success': False, 'message': str(e)}), 500