截图识别飞书个人任务清单
This commit is contained in:
452
web_app.py
Normal file
452
web_app.py
Normal file
@@ -0,0 +1,452 @@
|
||||
#!/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)
|
||||
Reference in New Issue
Block a user