#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ Screen2Feishu Web应用 结合前端页面与后端功能 """ import os import sys import json import time import base64 import logging from pathlib import Path from datetime import datetime from typing import Dict, Optional, List # 添加项目根目录到Python路径 project_root = Path(__file__).parent sys.path.insert(0, str(project_root)) from flask import Flask, render_template, request, jsonify, send_from_directory from flask_cors import CORS from werkzeug.utils import secure_filename from services.ai_service import AIService from services.feishu_service import FeishuService from utils.config_loader import load_config, validate_config from utils.user_cache import UserCacheManager from utils.logger import setup_logger # 初始化Flask应用 app = Flask(__name__, static_folder='static', template_folder='templates') CORS(app) # 配置上传文件大小限制 app.config['MAX_CONTENT_LENGTH'] = 16 * 1024 * 1024 # 16MB # 全局变量 config = None ai_service = None feishu_service = None user_cache = None logger = None def init_app(): """初始化应用""" global config, ai_service, feishu_service, user_cache, logger # 加载配置 try: config = load_config("config.yaml") validate_config(config) logger = setup_logger(config['system']['log_level'], config['system']['log_file']) logger.info("配置加载成功") except Exception as e: print(f"配置加载失败: {str(e)}") sys.exit(1) # 初始化服务 try: ai_service = AIService(config['ai']) feishu_service = FeishuService(config['feishu']) # 初始化用户缓存 cache_file = config['system']['user_matching']['cache_file'] user_cache = UserCacheManager(cache_file) logger.info("服务初始化成功") except Exception as e: logger.error(f"服务初始化失败: {str(e)}") sys.exit(1) # 创建必要的目录 Path("monitor_images").mkdir(exist_ok=True) Path("processed_images").mkdir(exist_ok=True) Path("data").mkdir(exist_ok=True) # 路由定义 @app.route('/') def index(): """首页 - 返回前端HTML页面""" return render_template('index.html') @app.route('/api/upload', methods=['POST']) def upload_image(): """上传图片并分析""" try: if 'image' not in request.files: return jsonify({'success': False, 'error': '没有上传文件'}), 400 file = request.files['image'] if file.filename == '': return jsonify({'success': False, 'error': '没有选择文件'}), 400 if not file.content_type.startswith('image/'): return jsonify({'success': False, 'error': '文件不是图片'}), 400 # 保存图片到monitor_images目录 filename = secure_filename(file.filename) timestamp = int(time.time()) filename = f"screenshot-{timestamp}-{filename}" filepath = Path("monitor_images") / filename file.save(filepath) logger.info(f"上传图片: {filename}") # 检查是否启用内存处理 memory_processing = config['system'].get('memory_processing', False) if memory_processing: # 内存处理模式 with open(filepath, 'rb') as f: image_bytes = f.read() # AI分析图片 ai_result = ai_service.analyze_image_from_bytes(image_bytes, filename) if not ai_result: return jsonify({'success': False, 'error': 'AI分析失败'}), 500 else: # 文件处理模式 ai_result = ai_service.analyze_image(str(filepath)) if not ai_result: return jsonify({'success': False, 'error': 'AI分析失败'}), 500 # 处理任务发起方 initiator_name = ai_result.get("initiator", "") if initiator_name and config['system']['user_matching']['enabled']: # 尝试匹配用户 user_match = _match_initiator(initiator_name) if user_match: if user_match.get('matched'): # 成功匹配到用户ID ai_result["任务发起方"] = {"id": user_match['user_id']} logger.info(f"成功匹配任务发起方: {initiator_name} -> {user_match['user_name']}") else: # 未匹配到用户,添加待确认标记 ai_result["task_description"] = f"[待确认发起人] {ai_result['task_description']}" ai_result["任务发起方.部门"] = f"待确认: {initiator_name}" logger.warning(f"未匹配到用户: {initiator_name},标记为待确认") # 添加到最近联系人 if user_cache: user_cache.add_recent_contact(initiator_name) else: # 匹配失败,使用随机选择或留空 if config['system']['user_matching']['fallback_to_random'] and user_cache: random_contact = user_cache.get_random_recent_contact() if random_contact: ai_result["task_description"] = f"[随机匹配] {ai_result['task_description']}" ai_result["任务发起方.部门"] = f"随机: {random_contact}" logger.info(f"随机匹配任务发起方: {random_contact}") else: ai_result["task_description"] = f"[待确认发起人] {ai_result['task_description']}" logger.warning(f"无最近联系人可用,标记为待确认: {initiator_name}") else: ai_result["task_description"] = f"[待确认发起人] {ai_result['task_description']}" logger.warning(f"未匹配到用户且未启用随机匹配: {initiator_name}") # 处理部门信息 if "department" in ai_result and ai_result["department"]: if "任务发起方.部门" not in ai_result: ai_result["任务发起方.部门"] = ai_result["department"] # 写入飞书 success = feishu_service.add_task(ai_result) if success: # 后处理文件 post_process = config['system'].get('post_process', 'keep') if post_process == 'delete': filepath.unlink() logger.info(f"已删除文件: {filename}") elif post_process == 'move': processed_folder = Path(config['system']['processed_folder']) processed_folder.mkdir(exist_ok=True) target_path = processed_folder / filename filepath.rename(target_path) logger.info(f"已移动文件到: {target_path}") return jsonify({ 'success': True, 'message': '任务已成功添加到飞书', 'ai_result': ai_result }) else: return jsonify({'success': False, 'error': '写入飞书失败'}), 500 except Exception as e: logger.error(f"处理图片失败: {str(e)}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/tasks', methods=['GET']) def get_tasks(): """获取任务列表""" try: # 这里可以添加从飞书获取任务列表的逻辑 # 暂时返回空列表 return jsonify({'success': True, 'tasks': []}) except Exception as e: logger.error(f"获取任务列表失败: {str(e)}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/user_cache', methods=['GET']) def get_user_cache(): """获取用户缓存信息""" try: if not user_cache: return jsonify({'success': False, 'error': '用户缓存未初始化'}), 500 stats = user_cache.get_cache_stats() return jsonify({'success': True, 'stats': stats}) except Exception as e: logger.error(f"获取用户缓存信息失败: {str(e)}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/config', methods=['GET']) def get_config(): """获取配置信息""" try: config_info = { 'memory_processing': config['system'].get('memory_processing', False), 'user_matching_enabled': config['system']['user_matching']['enabled'], 'fallback_to_random': config['system']['user_matching']['fallback_to_random'], 'cache_enabled': config['system']['user_matching']['cache_enabled'], 'post_process': config['system'].get('post_process', 'keep') } return jsonify({'success': True, 'config': config_info}) except Exception as e: logger.error(f"获取配置信息失败: {str(e)}") return jsonify({'success': False, 'error': str(e)}), 500 def _match_initiator(initiator_name: str) -> Optional[Dict]: """匹配任务发起人""" if not initiator_name or not config['system']['user_matching']['enabled']: return None # 确保用户缓存已初始化 if not user_cache: return None # 检查缓存是否过期,如果过期则重新获取用户列表 if user_cache.is_cache_expired(max_age_hours=24): logger.info("用户缓存已过期,重新获取用户列表") users = feishu_service._get_user_list() if users: user_cache.update_users(users) # 尝试匹配用户 matched_user = user_cache.match_user_by_name(initiator_name) if matched_user: return { 'matched': True, 'user_id': matched_user.get('user_id'), 'user_name': matched_user.get('name') } else: return { 'matched': False, 'user_name': initiator_name } @app.route('/api/screenshot', methods=['POST']) def capture_screenshot(): """截图上传接口""" try: if 'screenshot' not in request.files: return jsonify({'success': False, 'error': '没有上传文件'}), 400 file = request.files['screenshot'] if file.filename == '': return jsonify({'success': False, 'error': '没有选择文件'}), 400 # 保存截图 filename = secure_filename(file.filename) timestamp = int(time.time()) filename = f"screenshot-{timestamp}-{filename}" filepath = Path("monitor_images") / filename file.save(filepath) logger.info(f"接收截图: {filename}") # 调用分析接口 with open(filepath, 'rb') as f: image_bytes = f.read() # AI分析图片 ai_result = ai_service.analyze_image_from_bytes(image_bytes, filename) if not ai_result: return jsonify({'success': False, 'error': 'AI分析失败'}), 500 # 处理任务发起方 initiator_name = ai_result.get("initiator", "") if initiator_name and config['system']['user_matching']['enabled']: user_match = _match_initiator(initiator_name) if user_match: if user_match.get('matched'): ai_result["任务发起方"] = {"id": user_match['user_id']} logger.info(f"成功匹配任务发起方: {initiator_name} -> {user_match['user_name']}") else: ai_result["task_description"] = f"[待确认发起人] {ai_result['task_description']}" ai_result["任务发起方.部门"] = f"待确认: {initiator_name}" logger.warning(f"未匹配到用户: {initiator_name},标记为待确认") if user_cache: user_cache.add_recent_contact(initiator_name) else: if config['system']['user_matching']['fallback_to_random'] and user_cache: random_contact = user_cache.get_random_recent_contact() if random_contact: ai_result["task_description"] = f"[随机匹配] {ai_result['task_description']}" ai_result["任务发起方.部门"] = f"随机: {random_contact}" logger.info(f"随机匹配任务发起方: {random_contact}") else: ai_result["task_description"] = f"[待确认发起人] {ai_result['task_description']}" logger.warning(f"无最近联系人可用,标记为待确认: {initiator_name}") else: ai_result["task_description"] = f"[待确认发起人] {ai_result['task_description']}" logger.warning(f"未匹配到用户且未启用随机匹配: {initiator_name}") # 处理部门信息 if "department" in ai_result and ai_result["department"]: if "任务发起方.部门" not in ai_result: ai_result["任务发起方.部门"] = ai_result["department"] # 写入飞书 success = feishu_service.add_task(ai_result) if success: # 后处理文件 post_process = config['system'].get('post_process', 'keep') if post_process == 'delete': filepath.unlink() logger.info(f"已删除文件: {filename}") elif post_process == 'move': processed_folder = Path(config['system']['processed_folder']) processed_folder.mkdir(exist_ok=True) target_path = processed_folder / filename filepath.rename(target_path) logger.info(f"已移动文件到: {target_path}") return jsonify({ 'success': True, 'message': '任务已成功添加到飞书', 'ai_result': ai_result }) else: return jsonify({'success': False, 'error': '写入飞书失败'}), 500 except Exception as e: logger.error(f"处理截图失败: {str(e)}") return jsonify({'success': False, 'error': str(e)}), 500 @app.route('/api/monitor', methods=['POST']) def monitor_upload(): """监控文件夹上传接口""" try: if 'file' not in request.files: return jsonify({'success': False, 'error': '没有上传文件'}), 400 file = request.files['file'] if file.filename == '': return jsonify({'success': False, 'error': '没有选择文件'}), 400 # 保存文件到monitor_images filename = secure_filename(file.filename) filepath = Path("monitor_images") / filename file.save(filepath) logger.info(f"接收监控文件: {filename}") # 调用分析接口 with open(filepath, 'rb') as f: image_bytes = f.read() # AI分析图片 ai_result = ai_service.analyze_image_from_bytes(image_bytes, filename) if not ai_result: return jsonify({'success': False, 'error': 'AI分析失败'}), 500 # 处理任务发起方 initiator_name = ai_result.get("initiator", "") if initiator_name and config['system']['user_matching']['enabled']: user_match = _match_initiator(initiator_name) if user_match: if user_match.get('matched'): ai_result["任务发起方"] = {"id": user_match['user_id']} logger.info(f"成功匹配任务发起方: {initiator_name} -> {user_match['user_name']}") else: ai_result["task_description"] = f"[待确认发起人] {ai_result['task_description']}" ai_result["任务发起方.部门"] = f"待确认: {initiator_name}" logger.warning(f"未匹配到用户: {initiator_name},标记为待确认") if user_cache: user_cache.add_recent_contact(initiator_name) else: if config['system']['user_matching']['fallback_to_random'] and user_cache: random_contact = user_cache.get_random_recent_contact() if random_contact: ai_result["task_description"] = f"[随机匹配] {ai_result['task_description']}" ai_result["任务发起方.部门"] = f"随机: {random_contact}" logger.info(f"随机匹配任务发起方: {random_contact}") else: ai_result["task_description"] = f"[待确认发起人] {ai_result['task_description']}" logger.warning(f"无最近联系人可用,标记为待确认: {initiator_name}") else: ai_result["task_description"] = f"[待确认发起人] {ai_result['task_description']}" logger.warning(f"未匹配到用户且未启用随机匹配: {initiator_name}") # 处理部门信息 if "department" in ai_result and ai_result["department"]: if "任务发起方.部门" not in ai_result: ai_result["任务发起方.部门"] = ai_result["department"] # 写入飞书 success = feishu_service.add_task(ai_result) if success: # 后处理文件 post_process = config['system'].get('post_process', 'keep') if post_process == 'delete': filepath.unlink() logger.info(f"已删除文件: {filename}") elif post_process == 'move': processed_folder = Path(config['system']['processed_folder']) processed_folder.mkdir(exist_ok=True) target_path = processed_folder / filename filepath.rename(target_path) logger.info(f"已移动文件到: {target_path}") return jsonify({ 'success': True, 'message': '任务已成功添加到飞书', 'ai_result': ai_result }) else: return jsonify({'success': False, 'error': '写入飞书失败'}), 500 except Exception as e: logger.error(f"处理监控文件失败: {str(e)}") return jsonify({'success': False, 'error': str(e)}), 500 if __name__ == '__main__': # 初始化应用 init_app() # 启动Web服务器 logger.info("启动Web服务器...") app.run(host='0.0.0.0', port=5000, debug=False)