截图识别飞书个人任务清单
This commit is contained in:
54
utils/__init__.py
Normal file
54
utils/__init__.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
工具模块
|
||||
"""
|
||||
|
||||
from .config_loader import (
|
||||
load_config,
|
||||
validate_config,
|
||||
save_config,
|
||||
create_default_config,
|
||||
create_sample_config,
|
||||
get_config_summary
|
||||
)
|
||||
|
||||
from .logger import (
|
||||
setup_logger,
|
||||
get_logger,
|
||||
log_function_call,
|
||||
log_function_result,
|
||||
log_error_with_context,
|
||||
create_log_rotation_handler
|
||||
)
|
||||
|
||||
from .notifier import (
|
||||
send_notification,
|
||||
send_notification_with_icon,
|
||||
play_sound,
|
||||
notify_task_processed,
|
||||
notify_batch_result
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
# 配置加载器
|
||||
'load_config',
|
||||
'validate_config',
|
||||
'save_config',
|
||||
'create_default_config',
|
||||
'create_sample_config',
|
||||
'get_config_summary',
|
||||
|
||||
# 日志记录器
|
||||
'setup_logger',
|
||||
'get_logger',
|
||||
'log_function_call',
|
||||
'log_function_result',
|
||||
'log_error_with_context',
|
||||
'create_log_rotation_handler',
|
||||
|
||||
# 通知器
|
||||
'send_notification',
|
||||
'send_notification_with_icon',
|
||||
'play_sound',
|
||||
'notify_task_processed',
|
||||
'notify_batch_result'
|
||||
]
|
||||
200
utils/config_loader.py
Normal file
200
utils/config_loader.py
Normal file
@@ -0,0 +1,200 @@
|
||||
"""
|
||||
配置文件加载和验证工具
|
||||
"""
|
||||
|
||||
import yaml
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any
|
||||
|
||||
|
||||
def load_config(config_path: str) -> Dict[str, Any]:
|
||||
"""
|
||||
加载配置文件
|
||||
|
||||
Args:
|
||||
config_path: 配置文件路径
|
||||
|
||||
Returns:
|
||||
配置字典
|
||||
|
||||
Raises:
|
||||
FileNotFoundError: 配置文件不存在
|
||||
yaml.YAMLError: 配置文件格式错误
|
||||
"""
|
||||
config_file = Path(config_path)
|
||||
|
||||
if not config_file.exists():
|
||||
raise FileNotFoundError(f"配置文件不存在: {config_path}")
|
||||
|
||||
try:
|
||||
with open(config_file, 'r', encoding='utf-8') as f:
|
||||
config = yaml.safe_load(f)
|
||||
return config
|
||||
except yaml.YAMLError as e:
|
||||
raise yaml.YAMLError(f"配置文件格式错误: {str(e)}")
|
||||
|
||||
|
||||
def validate_config(config: Dict[str, Any]) -> None:
|
||||
"""
|
||||
验证配置文件的完整性和有效性
|
||||
|
||||
Args:
|
||||
config: 配置字典
|
||||
|
||||
Raises:
|
||||
ValueError: 配置验证失败
|
||||
"""
|
||||
# 检查必需的顶级配置块
|
||||
required_sections = ['ai', 'feishu', 'system']
|
||||
for section in required_sections:
|
||||
if section not in config:
|
||||
raise ValueError(f"缺少必需的配置块: {section}")
|
||||
|
||||
# 验证AI配置
|
||||
ai_config = config['ai']
|
||||
if not ai_config.get('api_key'):
|
||||
raise ValueError("AI配置缺少 api_key")
|
||||
if not ai_config.get('base_url'):
|
||||
raise ValueError("AI配置缺少 base_url")
|
||||
if not ai_config.get('model'):
|
||||
raise ValueError("AI配置缺少 model")
|
||||
|
||||
# 验证飞书配置
|
||||
feishu_config = config['feishu']
|
||||
if not feishu_config.get('app_id'):
|
||||
raise ValueError("飞书配置缺少 app_id")
|
||||
if not feishu_config.get('app_secret'):
|
||||
raise ValueError("飞书配置缺少 app_secret")
|
||||
if not feishu_config.get('app_token'):
|
||||
raise ValueError("飞书配置缺少 app_token")
|
||||
if not feishu_config.get('table_id'):
|
||||
raise ValueError("飞书配置缺少 table_id")
|
||||
|
||||
# 验证系统配置
|
||||
system_config = config['system']
|
||||
if not system_config.get('watch_folder'):
|
||||
raise ValueError("系统配置缺少 watch_folder")
|
||||
|
||||
# 验证后处理配置
|
||||
post_process = system_config.get('post_process', 'keep')
|
||||
valid_post_process = ['delete', 'move', 'keep']
|
||||
if post_process not in valid_post_process:
|
||||
raise ValueError(f"post_process 必须是 {valid_post_process} 之一,当前值: {post_process}")
|
||||
|
||||
if post_process == 'move' and not system_config.get('processed_folder'):
|
||||
raise ValueError("当 post_process 为 'move' 时,必须配置 processed_folder")
|
||||
|
||||
# 验证路径格式
|
||||
watch_folder = Path(system_config['watch_folder'])
|
||||
if not watch_folder.is_absolute():
|
||||
# 如果是相对路径,转换为绝对路径
|
||||
watch_folder = Path.cwd() / watch_folder
|
||||
|
||||
# 检查监控文件夹是否存在(允许不存在,会在运行时创建)
|
||||
if watch_folder.exists() and not watch_folder.is_dir():
|
||||
raise ValueError(f"watch_folder 必须是一个目录: {watch_folder}")
|
||||
|
||||
# 如果配置了移动目标文件夹,检查其路径
|
||||
if post_process == 'move':
|
||||
processed_folder = Path(system_config['processed_folder'])
|
||||
if not processed_folder.is_absolute():
|
||||
processed_folder = Path.cwd() / processed_folder
|
||||
|
||||
if processed_folder.exists() and not processed_folder.is_dir():
|
||||
raise ValueError(f"processed_folder 必须是一个目录: {processed_folder}")
|
||||
|
||||
|
||||
def save_config(config: Dict[str, Any], config_path: str) -> None:
|
||||
"""
|
||||
保存配置文件
|
||||
|
||||
Args:
|
||||
config: 配置字典
|
||||
config_path: 配置文件路径
|
||||
"""
|
||||
config_file = Path(config_path)
|
||||
|
||||
# 确保目录存在
|
||||
config_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
with open(config_file, 'w', encoding='utf-8') as f:
|
||||
yaml.dump(config, f, default_flow_style=False, allow_unicode=True)
|
||||
|
||||
|
||||
def create_default_config() -> Dict[str, Any]:
|
||||
"""
|
||||
创建默认配置
|
||||
|
||||
Returns:
|
||||
默认配置字典
|
||||
"""
|
||||
return {
|
||||
'ai': {
|
||||
'api_key': 'sk-xxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
'base_url': 'https://api.siliconflow.cn/v1',
|
||||
'model': 'gemini-2.5-pro'
|
||||
},
|
||||
'feishu': {
|
||||
'app_id': 'cli_xxxxxxxxxxxx',
|
||||
'app_secret': 'xxxxxxxxxxxxxxxxxxxxxxxx',
|
||||
'app_token': 'bascnxxxxxxxxxxxxxxx',
|
||||
'table_id': 'tblxxxxxxxxxxxx'
|
||||
},
|
||||
'system': {
|
||||
'watch_folder': './monitor_images',
|
||||
'post_process': 'move',
|
||||
'processed_folder': './processed_images'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
def create_sample_config(config_path: str = 'config.example.yaml') -> None:
|
||||
"""
|
||||
创建示例配置文件
|
||||
|
||||
Args:
|
||||
config_path: 示例配置文件路径
|
||||
"""
|
||||
default_config = create_default_config()
|
||||
save_config(default_config, config_path)
|
||||
print(f"示例配置文件已创建: {config_path}")
|
||||
|
||||
|
||||
def get_config_summary(config: Dict[str, Any]) -> str:
|
||||
"""
|
||||
获取配置摘要信息
|
||||
|
||||
Args:
|
||||
config: 配置字典
|
||||
|
||||
Returns:
|
||||
配置摘要字符串
|
||||
"""
|
||||
summary = []
|
||||
summary.append("配置摘要:")
|
||||
summary.append(f" AI模型: {config['ai']['model']}")
|
||||
summary.append(f" AI服务: {config['ai']['base_url']}")
|
||||
summary.append(f" 监控文件夹: {config['system']['watch_folder']}")
|
||||
summary.append(f" 后处理方式: {config['system'].get('post_process', 'keep')}")
|
||||
|
||||
if config['system'].get('post_process') == 'move':
|
||||
summary.append(f" 处理后文件夹: {config['system']['processed_folder']}")
|
||||
|
||||
return '\n'.join(summary)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试配置验证
|
||||
try:
|
||||
# 创建示例配置
|
||||
create_sample_config('config.example.yaml')
|
||||
print("示例配置创建成功!")
|
||||
|
||||
# 加载并验证示例配置
|
||||
config = load_config('config.example.yaml')
|
||||
validate_config(config)
|
||||
print("配置验证通过!")
|
||||
print(get_config_summary(config))
|
||||
|
||||
except Exception as e:
|
||||
print(f"配置测试失败: {e}")
|
||||
185
utils/logger.py
Normal file
185
utils/logger.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
日志记录工具
|
||||
"""
|
||||
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def setup_logger(
|
||||
name: str = 'screen2feishu',
|
||||
log_file: Optional[str] = None,
|
||||
level: str = 'INFO',
|
||||
format: str = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
|
||||
) -> logging.Logger:
|
||||
"""
|
||||
设置日志记录器
|
||||
|
||||
Args:
|
||||
name: 日志记录器名称
|
||||
log_file: 日志文件路径,如果为None则只输出到控制台
|
||||
level: 日志级别 (DEBUG, INFO, WARNING, ERROR, CRITICAL)
|
||||
format: 日志格式
|
||||
|
||||
Returns:
|
||||
配置好的日志记录器
|
||||
"""
|
||||
# 创建日志记录器
|
||||
logger = logging.getLogger(name)
|
||||
|
||||
# 避免重复配置
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
# 设置日志级别
|
||||
level_map = {
|
||||
'DEBUG': logging.DEBUG,
|
||||
'INFO': logging.INFO,
|
||||
'WARNING': logging.WARNING,
|
||||
'ERROR': logging.ERROR,
|
||||
'CRITICAL': logging.CRITICAL
|
||||
}
|
||||
|
||||
log_level = level_map.get(level.upper(), logging.INFO)
|
||||
logger.setLevel(log_level)
|
||||
|
||||
# 创建格式化器
|
||||
formatter = logging.Formatter(format, datefmt='%Y-%m-%d %H:%M:%S')
|
||||
|
||||
# 创建控制台处理器
|
||||
# 在Windows系统上,确保使用UTF-8编码输出
|
||||
if sys.platform == 'win32':
|
||||
import io
|
||||
console_handler = logging.StreamHandler(io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8'))
|
||||
else:
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# 创建文件处理器(如果指定了日志文件)
|
||||
if log_file:
|
||||
log_path = Path(log_file)
|
||||
|
||||
# 确保日志目录存在
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_handler = logging.FileHandler(log_path, encoding='utf-8')
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def get_logger(name: str = 'screen2feishu') -> logging.Logger:
|
||||
"""
|
||||
获取已配置的日志记录器
|
||||
|
||||
Args:
|
||||
name: 日志记录器名称
|
||||
|
||||
Returns:
|
||||
日志记录器
|
||||
"""
|
||||
return logging.getLogger(name)
|
||||
|
||||
|
||||
def log_function_call(logger: logging.Logger, func_name: str, *args, **kwargs):
|
||||
"""
|
||||
记录函数调用日志
|
||||
|
||||
Args:
|
||||
logger: 日志记录器
|
||||
func_name: 函数名
|
||||
*args: 位置参数
|
||||
**kwargs: 关键字参数
|
||||
"""
|
||||
args_str = ', '.join(repr(arg) for arg in args)
|
||||
kwargs_str = ', '.join(f'{k}={repr(v)}' for k, v in kwargs.items())
|
||||
all_args = ', '.join(filter(None, [args_str, kwargs_str]))
|
||||
|
||||
logger.debug(f"调用函数: {func_name}({all_args})")
|
||||
|
||||
|
||||
def log_function_result(logger: logging.Logger, func_name: str, result):
|
||||
"""
|
||||
记录函数返回结果日志
|
||||
|
||||
Args:
|
||||
logger: 日志记录器
|
||||
func_name: 函数名
|
||||
result: 函数返回结果
|
||||
"""
|
||||
logger.debug(f"函数 {func_name} 返回: {repr(result)}")
|
||||
|
||||
|
||||
def log_error_with_context(logger: logging.Logger, error: Exception, context: str = ""):
|
||||
"""
|
||||
记录错误日志,包含上下文信息
|
||||
|
||||
Args:
|
||||
logger: 日志记录器
|
||||
error: 异常对象
|
||||
context: 错误上下文信息
|
||||
"""
|
||||
if context:
|
||||
logger.error(f"{context}: {str(error)}")
|
||||
else:
|
||||
logger.error(f"错误: {str(error)}")
|
||||
|
||||
|
||||
def create_log_rotation_handler(
|
||||
log_file: str,
|
||||
max_bytes: int = 10 * 1024 * 1024, # 10MB
|
||||
backup_count: int = 5
|
||||
) -> logging.Handler:
|
||||
"""
|
||||
创建支持日志轮转的文件处理器
|
||||
|
||||
Args:
|
||||
log_file: 日志文件路径
|
||||
max_bytes: 单个日志文件最大字节数
|
||||
backup_count: 备份文件数量
|
||||
|
||||
Returns:
|
||||
文件处理器
|
||||
"""
|
||||
from logging.handlers import RotatingFileHandler
|
||||
|
||||
log_path = Path(log_file)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
handler = RotatingFileHandler(
|
||||
log_file,
|
||||
maxBytes=max_bytes,
|
||||
backupCount=backup_count,
|
||||
encoding='utf-8'
|
||||
)
|
||||
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
handler.setFormatter(formatter)
|
||||
|
||||
return handler
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试日志功能
|
||||
logger = setup_logger(
|
||||
name='test_logger',
|
||||
log_file='test.log',
|
||||
level='DEBUG'
|
||||
)
|
||||
|
||||
logger.debug("这是一条调试信息")
|
||||
logger.info("这是一条普通信息")
|
||||
logger.warning("这是一条警告信息")
|
||||
logger.error("这是一条错误信息")
|
||||
|
||||
# 测试函数调用日志
|
||||
log_function_call(logger, "test_function", "arg1", "arg2", key="value")
|
||||
log_function_result(logger, "test_function", {"result": "success"})
|
||||
log_error_with_context(logger, Exception("测试错误"), "处理数据时")
|
||||
306
utils/notifier.py
Normal file
306
utils/notifier.py
Normal file
@@ -0,0 +1,306 @@
|
||||
"""
|
||||
通知工具 - 支持桌面通知、声音提示等
|
||||
"""
|
||||
|
||||
import platform
|
||||
import subprocess
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def send_notification(title: str, message: str, urgency: str = "normal") -> bool:
|
||||
"""
|
||||
发送桌面通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
message: 通知内容
|
||||
urgency: 通知紧急程度 (low, normal, high)
|
||||
|
||||
Returns:
|
||||
是否成功发送
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
try:
|
||||
if system == "Windows":
|
||||
# Windows 使用 PowerShell 发送通知
|
||||
ps_script = f"""
|
||||
[Windows.UI.Notifications.ToastNotificationManager, Windows.UI.Notifications, ContentType=WindowsRuntime] > $null
|
||||
$template = [Windows.UI.Notifications.ToastNotificationManager]::GetTemplateContent([Windows.UI.Notifications.ToastTemplateType]::ToastText02)
|
||||
$template.SelectSingleNode("//text[@id='1']").AppendChild($template.CreateTextNode('{title}')) > $null
|
||||
$template.SelectSingleNode("//text[@id='2']").AppendChild($template.CreateTextNode('{message}')) > $null
|
||||
$toast = [Windows.UI.Notifications.ToastNotification]::new($template)
|
||||
[Windows.UI.Notifications.ToastNotificationManager]::CreateToastNotifier("Screen2Feishu").Show($toast)
|
||||
"""
|
||||
|
||||
# 使用 PowerShell 执行
|
||||
subprocess.run(
|
||||
["powershell", "-Command", ps_script],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return True
|
||||
|
||||
elif system == "Darwin": # macOS
|
||||
# macOS 使用 osascript 发送通知
|
||||
script = f'display notification "{message}" with title "{title}"'
|
||||
subprocess.run(
|
||||
["osascript", "-e", script],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return True
|
||||
|
||||
elif system == "Linux":
|
||||
# Linux 使用 notify-send
|
||||
try:
|
||||
subprocess.run(
|
||||
["notify-send", "-u", urgency, title, message],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
# 如果 notify-send 不可用,尝试使用 zenity
|
||||
try:
|
||||
subprocess.run(
|
||||
["zenity", "--info", "--title", title, "--text", message],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"发送通知失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def send_notification_with_icon(title: str, message: str, icon_path: Optional[str] = None) -> bool:
|
||||
"""
|
||||
发送带图标的桌面通知
|
||||
|
||||
Args:
|
||||
title: 通知标题
|
||||
message: 通知内容
|
||||
icon_path: 图标文件路径
|
||||
|
||||
Returns:
|
||||
是否成功发送
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
try:
|
||||
if system == "Linux" and icon_path:
|
||||
# Linux 支持图标
|
||||
subprocess.run(
|
||||
["notify-send", "-u", "normal", "-i", icon_path, title, message],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return True
|
||||
else:
|
||||
# 其他系统使用普通通知
|
||||
return send_notification(title, message)
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"发送带图标通知失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def play_sound(sound_type: str = "success") -> bool:
|
||||
"""
|
||||
播放系统声音
|
||||
|
||||
Args:
|
||||
sound_type: 声音类型 (success, error, warning, info)
|
||||
|
||||
Returns:
|
||||
是否成功播放
|
||||
"""
|
||||
system = platform.system()
|
||||
|
||||
try:
|
||||
if system == "Windows":
|
||||
# Windows 使用 PowerShell 播放系统声音
|
||||
sound_map = {
|
||||
"success": "SystemAsterisk",
|
||||
"error": "SystemHand",
|
||||
"warning": "SystemExclamation",
|
||||
"info": "SystemDefault"
|
||||
}
|
||||
|
||||
sound = sound_map.get(sound_type, "SystemDefault")
|
||||
ps_script = f'[System.Media.SystemSounds]::{sound}.Play()'
|
||||
|
||||
subprocess.run(
|
||||
["powershell", "-Command", ps_script],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return True
|
||||
|
||||
elif system == "Darwin": # macOS
|
||||
# macOS 使用 afplay 播放系统声音
|
||||
sound_map = {
|
||||
"success": "/System/Library/Sounds/Purr.aiff",
|
||||
"error": "/System/Library/Sounds/Basso.aiff",
|
||||
"warning": "/System/Library/Sounds/Funk.aiff",
|
||||
"info": "/System/Library/Sounds/Glass.aiff"
|
||||
}
|
||||
|
||||
sound_file = sound_map.get(sound_type, "/System/Library/Sounds/Glass.aiff")
|
||||
|
||||
# 异步播放声音
|
||||
subprocess.Popen(
|
||||
["afplay", sound_file],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL
|
||||
)
|
||||
return True
|
||||
|
||||
elif system == "Linux":
|
||||
# Linux 使用 paplay 或 aplay 播放声音
|
||||
# 这里使用简单的 beep 声音
|
||||
try:
|
||||
# 尝试使用 beep
|
||||
subprocess.run(
|
||||
["beep"],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
return True
|
||||
except FileNotFoundError:
|
||||
# 如果 beep 不可用,尝试使用 paplay
|
||||
try:
|
||||
# 创建临时的声音文件
|
||||
import tempfile
|
||||
import wave
|
||||
import struct
|
||||
|
||||
# 创建简单的正弦波声音
|
||||
with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as f:
|
||||
# 写入 WAV 文件头
|
||||
sample_rate = 44100
|
||||
duration = 0.2
|
||||
num_samples = int(sample_rate * duration)
|
||||
|
||||
wav_file = wave.open(f.name, 'w')
|
||||
wav_file.setnchannels(1)
|
||||
wav_file.setsampwidth(2)
|
||||
wav_file.setframerate(sample_rate)
|
||||
|
||||
# 生成正弦波
|
||||
for i in range(num_samples):
|
||||
value = int(32767 * 0.3 * (1 if i < num_samples // 2 else 0))
|
||||
wav_file.writeframes(struct.pack('<h', value))
|
||||
|
||||
wav_file.close()
|
||||
|
||||
# 播放声音
|
||||
subprocess.run(
|
||||
["paplay", f.name],
|
||||
capture_output=True,
|
||||
timeout=5
|
||||
)
|
||||
|
||||
# 删除临时文件
|
||||
import os
|
||||
os.unlink(f.name)
|
||||
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
else:
|
||||
return False
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"播放声音失败: {str(e)}")
|
||||
return False
|
||||
|
||||
|
||||
def notify_task_processed(success: bool, file_name: str, count: int = 1) -> bool:
|
||||
"""
|
||||
通知任务处理结果
|
||||
|
||||
Args:
|
||||
success: 是否成功
|
||||
file_name: 文件名
|
||||
count: 处理数量
|
||||
|
||||
Returns:
|
||||
是否成功发送通知
|
||||
"""
|
||||
if success:
|
||||
title = " 任务处理成功"
|
||||
message = f"已成功处理 {count} 个文件: {file_name}"
|
||||
sound_type = "success"
|
||||
else:
|
||||
title = " 任务处理失败"
|
||||
message = f"处理文件失败: {file_name}"
|
||||
sound_type = "error"
|
||||
|
||||
# 发送桌面通知
|
||||
notification_sent = send_notification(title, message)
|
||||
|
||||
# 播放声音提示
|
||||
sound_played = play_sound(sound_type)
|
||||
|
||||
return notification_sent or sound_played
|
||||
|
||||
|
||||
def notify_batch_result(results: dict) -> bool:
|
||||
"""
|
||||
通知批量处理结果
|
||||
|
||||
Args:
|
||||
results: 处理结果字典 {文件名: 是否成功}
|
||||
|
||||
Returns:
|
||||
是否成功发送通知
|
||||
"""
|
||||
success_count = sum(1 for v in results.values() if v)
|
||||
total_count = len(results)
|
||||
|
||||
if success_count == total_count:
|
||||
title = " 批量处理完成"
|
||||
message = f"所有 {total_count} 个文件处理成功"
|
||||
sound_type = "success"
|
||||
elif success_count == 0:
|
||||
title = " 批量处理失败"
|
||||
message = f"所有 {total_count} 个文件处理失败"
|
||||
sound_type = "error"
|
||||
else:
|
||||
title = "⚠️ 批量处理部分成功"
|
||||
message = f"{success_count}/{total_count} 个文件处理成功"
|
||||
sound_type = "warning"
|
||||
|
||||
# 发送桌面通知
|
||||
notification_sent = send_notification(title, message)
|
||||
|
||||
# 播放声音提示
|
||||
sound_played = play_sound(sound_type)
|
||||
|
||||
return notification_sent or sound_played
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# 测试通知功能
|
||||
import logging
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
print("测试桌面通知...")
|
||||
success = send_notification("测试通知", "这是一个测试通知消息")
|
||||
print(f"通知发送: {'成功' if success else '失败'}")
|
||||
|
||||
print("测试声音播放...")
|
||||
success = play_sound("success")
|
||||
print(f"声音播放: {'成功' if success else '失败'}")
|
||||
|
||||
print("测试任务处理通知...")
|
||||
success = notify_task_processed(True, "test_image.png")
|
||||
print(f"任务通知: {'成功' if success else '失败'}")
|
||||
211
utils/user_cache.py
Normal file
211
utils/user_cache.py
Normal file
@@ -0,0 +1,211 @@
|
||||
import json
|
||||
import time
|
||||
import random
|
||||
from pathlib import Path
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
import logging
|
||||
|
||||
|
||||
class UserCacheManager:
|
||||
"""用户缓存管理器,用于管理飞书用户信息和最近联系人"""
|
||||
|
||||
def __init__(self, cache_file: str = "./data/user_cache.json"):
|
||||
"""
|
||||
初始化用户缓存管理器
|
||||
|
||||
Args:
|
||||
cache_file: 缓存文件路径
|
||||
"""
|
||||
self.cache_file = Path(cache_file)
|
||||
self.cache_file.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
# 缓存数据结构
|
||||
self.users: List[Dict] = [] # 用户列表
|
||||
self.recent_contacts: List[str] = [] # 最近联系人名字列表
|
||||
self.last_update_time: float = 0 # 最后更新时间
|
||||
|
||||
# 加载缓存
|
||||
self._load_cache()
|
||||
|
||||
def _load_cache(self):
|
||||
"""从文件加载缓存"""
|
||||
try:
|
||||
if self.cache_file.exists():
|
||||
with open(self.cache_file, 'r', encoding='utf-8') as f:
|
||||
data = json.load(f)
|
||||
self.users = data.get('users', [])
|
||||
self.recent_contacts = data.get('recent_contacts', [])
|
||||
self.last_update_time = data.get('last_update_time', 0)
|
||||
logging.info(f"已加载用户缓存,共 {len(self.users)} 个用户")
|
||||
else:
|
||||
logging.info("未找到用户缓存文件,将创建新的缓存")
|
||||
except Exception as e:
|
||||
logging.error(f"加载用户缓存失败: {str(e)}")
|
||||
self.users = []
|
||||
self.recent_contacts = []
|
||||
self.last_update_time = 0
|
||||
|
||||
def _save_cache(self):
|
||||
"""保存缓存到文件"""
|
||||
try:
|
||||
data = {
|
||||
'users': self.users,
|
||||
'recent_contacts': self.recent_contacts,
|
||||
'last_update_time': time.time()
|
||||
}
|
||||
with open(self.cache_file, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||
logging.info(f"已保存用户缓存到 {self.cache_file}")
|
||||
except Exception as e:
|
||||
logging.error(f"保存用户缓存失败: {str(e)}")
|
||||
|
||||
def update_users(self, users: List[Dict]):
|
||||
"""
|
||||
更新用户列表
|
||||
|
||||
Args:
|
||||
users: 飞书用户列表,每个用户包含 name, user_id 等字段
|
||||
"""
|
||||
self.users = users
|
||||
self.last_update_time = time.time()
|
||||
self._save_cache()
|
||||
logging.info(f"已更新用户列表,共 {len(users)} 个用户")
|
||||
|
||||
def add_recent_contact(self, name: str):
|
||||
"""
|
||||
添加最近联系人
|
||||
|
||||
Args:
|
||||
name: 联系人姓名
|
||||
"""
|
||||
if not name or name.strip() == "":
|
||||
return
|
||||
|
||||
name = name.strip()
|
||||
|
||||
# 如果已存在,先移除
|
||||
if name in self.recent_contacts:
|
||||
self.recent_contacts.remove(name)
|
||||
|
||||
# 添加到列表开头
|
||||
self.recent_contacts.insert(0, name)
|
||||
|
||||
# 限制列表长度(最多保留50个)
|
||||
if len(self.recent_contacts) > 50:
|
||||
self.recent_contacts = self.recent_contacts[:50]
|
||||
|
||||
self._save_cache()
|
||||
logging.debug(f"已添加最近联系人: {name}")
|
||||
|
||||
def match_user_by_name(self, name: str) -> Optional[Dict]:
|
||||
"""
|
||||
根据名字匹配用户
|
||||
|
||||
Args:
|
||||
name: 要匹配的名字
|
||||
|
||||
Returns:
|
||||
匹配的用户信息,如果未找到则返回None
|
||||
"""
|
||||
if not name or not self.users:
|
||||
return None
|
||||
|
||||
name = name.strip()
|
||||
|
||||
# 精确匹配
|
||||
for user in self.users:
|
||||
if user.get('name') == name:
|
||||
return user
|
||||
|
||||
# 模糊匹配(包含关系)
|
||||
for user in self.users:
|
||||
user_name = user.get('name', '')
|
||||
if user_name and (name in user_name or user_name in name):
|
||||
return user
|
||||
|
||||
return None
|
||||
|
||||
def get_random_recent_contact(self) -> Optional[str]:
|
||||
"""
|
||||
从最近联系人中随机选择一个
|
||||
|
||||
Returns:
|
||||
随机选择的联系人名字,如果没有则返回None
|
||||
"""
|
||||
if not self.recent_contacts:
|
||||
return None
|
||||
|
||||
# 随机选择一个
|
||||
return random.choice(self.recent_contacts)
|
||||
|
||||
def get_user_suggestions(self, name: str) -> List[Dict]:
|
||||
"""
|
||||
获取名字相似的用户建议
|
||||
|
||||
Args:
|
||||
name: 要匹配的名字
|
||||
|
||||
Returns:
|
||||
相似的用户列表
|
||||
"""
|
||||
if not name or not self.users:
|
||||
return []
|
||||
|
||||
name = name.strip().lower()
|
||||
suggestions = []
|
||||
|
||||
for user in self.users:
|
||||
user_name = user.get('name', '')
|
||||
user_name_lower = user_name.lower()
|
||||
|
||||
# 计算相似度(简单的包含关系)
|
||||
if name in user_name_lower or user_name_lower in name:
|
||||
suggestions.append(user)
|
||||
elif name[0] == user_name_lower[0] if user_name_lower else False:
|
||||
suggestions.append(user)
|
||||
|
||||
# 限制返回数量
|
||||
return suggestions[:5]
|
||||
|
||||
def is_cache_expired(self, max_age_hours: int = 24) -> bool:
|
||||
"""
|
||||
检查缓存是否过期
|
||||
|
||||
Args:
|
||||
max_age_hours: 最大缓存时间(小时)
|
||||
|
||||
Returns:
|
||||
是否过期
|
||||
"""
|
||||
if self.last_update_time == 0:
|
||||
return True
|
||||
|
||||
age_hours = (time.time() - self.last_update_time) / 3600
|
||||
return age_hours > max_age_hours
|
||||
|
||||
def clear_cache(self):
|
||||
"""清除缓存"""
|
||||
self.users = []
|
||||
self.recent_contacts = []
|
||||
self.last_update_time = 0
|
||||
self._save_cache()
|
||||
logging.info("已清除用户缓存")
|
||||
|
||||
def get_cache_stats(self) -> Dict:
|
||||
"""
|
||||
获取缓存统计信息
|
||||
|
||||
Returns:
|
||||
缓存统计信息
|
||||
"""
|
||||
age_hours = 0
|
||||
if self.last_update_time > 0:
|
||||
age_hours = (time.time() - self.last_update_time) / 3600
|
||||
|
||||
return {
|
||||
'user_count': len(self.users),
|
||||
'recent_contact_count': len(self.recent_contacts),
|
||||
'last_update_time': self.last_update_time,
|
||||
'cache_age_hours': round(age_hours, 2),
|
||||
'is_expired': self.is_cache_expired()
|
||||
}
|
||||
Reference in New Issue
Block a user