Files

452 lines
18 KiB
Python
Raw Permalink Normal View History

2026-03-18 15:15:52 +08:00
#!/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)