POE2消息转发,初始版本

This commit is contained in:
hitmant 2025-02-28 14:51:12 +08:00
commit dd59c2bb00
6 changed files with 506 additions and 0 deletions

61
.gitignore vendored Normal file
View File

@ -0,0 +1,61 @@
# Python
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
env/
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
*.egg-info/
.installed.cfg
*.egg
# PyInstaller
*.manifest
*.spec
build/
dist/
# Logs
*.log
logs/
poe_forwarder.log
# Config
config.json
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# Windows
Thumbs.db
ehthumbs.db
Desktop.ini
$RECYCLE.BIN/
# macOS
.DS_Store
.AppleDouble
.LSOverride
._*
# Virtual Environment
venv/
.env/
.venv/
ENV/

15
build.py Normal file
View File

@ -0,0 +1,15 @@
import PyInstaller.__main__
import os
# 确保在脚本所在目录运行
os.chdir(os.path.dirname(os.path.abspath(__file__)))
PyInstaller.__main__.run([
'main.py', # 主程序文件
'--onefile', # 打包成单个文件
'--name=POE_Message_Forwarder', # 输出文件名
'--clean', # 清理临时文件
'--noconsole', # 不显示控制台窗口
'--hidden-import=PIL._tkinter', # 确保PIL正确导入
'--hidden-import=PIL._imaging',
])

89
config.py Normal file
View File

@ -0,0 +1,89 @@
import os
import json
import sys
import tkinter as tk
from tkinter import messagebox
# 默认配置
DEFAULT_CONFIG = {
# Path of Exile客户端日志路径
"CLIENT_LOG_PATH": "C:/Program Files (x86)/Grinding Gear Games/Path of Exile 2/logs/Client.txt",
# 消息发送API配置
"API_URL": "https://al.xj.rs/api/v1/send",
"API_KEY": "", # 需要用户填写
# 交易通知配置
"TRADE_MESSAGES_CONFIG": {
"enabled": True, # 是否启用交易通知
"message_forward": True, # 是否转发到消息服务
},
# 私聊消息相关配置
"CHAT_MESSAGES": {
"enabled": True, # 是否启用私聊转发
"message_forward": True, # 是否转发到消息服务
}
}
CONFIG_FILE = "config.json"
def show_error(title, message):
"""显示错误消息对话框"""
root = tk.Tk()
root.withdraw() # 隐藏主窗口
messagebox.showerror(title, message)
root.destroy()
sys.exit(1)
def load_config():
"""加载或创建配置文件"""
if not os.path.exists(CONFIG_FILE):
# 创建默认配置文件
try:
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
json.dump(DEFAULT_CONFIG, f, indent=4, ensure_ascii=False)
show_error(
"配置文件创建成功",
f"已创建默认配置文件: {CONFIG_FILE}\n请编辑配置文件填写必要信息后重启程序"
)
except Exception as e:
show_error(
"错误",
f"创建配置文件失败: {str(e)}"
)
try:
# 读取配置文件
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
config = json.load(f)
# 检查必要的配置项
if not config.get("API_KEY"):
show_error(
"配置错误",
"请在配置文件中设置API_KEY后重启程序"
)
return config
except json.JSONDecodeError:
show_error(
"配置错误",
"配置文件格式错误请检查JSON格式是否正确"
)
except Exception as e:
show_error(
"错误",
f"读取配置文件失败: {str(e)}"
)
# 加载配置
config = load_config()
# 导出配置项
CLIENT_LOG_PATH = config["CLIENT_LOG_PATH"]
API_URL = config["API_URL"]
API_KEY = config["API_KEY"]
TRADE_MESSAGES_CONFIG = config["TRADE_MESSAGES_CONFIG"]
CHAT_MESSAGES = config["CHAT_MESSAGES"]

299
main.py Normal file
View File

@ -0,0 +1,299 @@
import os
import time
import re
import logging
from config import CLIENT_LOG_PATH, CHAT_MESSAGES, TRADE_MESSAGES_CONFIG
from message_sender import MessageSender
import threading
import signal
import pystray
from PIL import Image, ImageDraw
import tempfile
# 配置日志
def setup_logging():
log_file = "poe_forwarder.log"
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s - %(levelname)s - %(message)s',
handlers=[
logging.FileHandler(log_file, encoding='utf-8'),
logging.StreamHandler()
]
)
return logging.getLogger(__name__)
def create_icon():
"""创建一个更美观的图标"""
width = 64
height = 64
# 创建新图像使用RGBA支持透明
image = Image.new('RGBA', (width, height), (0, 0, 0, 0))
dc = ImageDraw.Draw(image)
# 定义颜色
primary_color = (65, 105, 225) # 皇家蓝
secondary_color = (30, 144, 255) # 道奇蓝
# 绘制主圆形
dc.ellipse([4, 4, width-4, height-4], fill=primary_color)
# 绘制内部装饰
# 绘制字母"P"
font_size = 32
try:
from PIL import ImageFont
font = ImageFont.truetype("arial.ttf", font_size)
except:
font = None
text = "P"
text_bbox = dc.textbbox((0, 0), text, font=font)
text_width = text_bbox[2] - text_bbox[0]
text_height = text_bbox[3] - text_bbox[1]
x = (width - text_width) // 2
y = (height - text_height) // 2
dc.text((x, y), text, fill='white', font=font)
return image
class TrayIcon:
def __init__(self, logger):
self.logger = logger
self.running = True
self.icon = None
self.create_icon()
def create_icon(self):
image = create_icon()
menu = (
pystray.MenuItem(
"POE消息转发器",
lambda: None,
enabled=False
),
pystray.Menu.SEPARATOR,
pystray.MenuItem(
"✓ 正在运行",
lambda: None,
enabled=False
),
pystray.MenuItem(
"📝 查看日志",
self.open_log
),
pystray.Menu.SEPARATOR,
pystray.MenuItem(
"⚙ 打开配置",
self.open_config
),
pystray.MenuItem(
"❌ 退出",
self.stop_application
)
)
self.icon = pystray.Icon(
"poe_forwarder",
image,
"POE消息转发器",
menu
)
def open_log(self):
"""打开日志文件"""
try:
os.startfile("poe_forwarder.log")
except Exception as e:
self.logger.error(f"打开日志文件失败: {str(e)}")
def open_config(self):
"""打开配置文件"""
try:
os.startfile("config.json")
except Exception as e:
self.logger.error(f"打开配置文件失败: {str(e)}")
def stop_application(self):
"""停止应用"""
self.logger.info("从托盘菜单退出程序")
self.running = False
self.icon.stop()
def run(self):
"""运行托盘图标"""
self.icon.run()
class TradeNotificationHandler:
def __init__(self, message_sender, logger):
self.sender = message_sender
self.last_position = 0
self.logger = logger
def check_new_trades(self):
try:
with open(CLIENT_LOG_PATH, 'r', encoding='utf-8', errors='ignore') as f:
# 获取当前文件大小
f.seek(0, 2)
current_position = f.tell()
# 如果文件大小变小了,说明文件被重置
if current_position < self.last_position:
self.logger.info(f"检测到文件重置")
self.last_position = 0 # 重置到文件开始
# 如果有新内容
if current_position > self.last_position:
# 只读取新增的内容
f.seek(self.last_position)
new_content = f.read()
# 更新位置(在处理内容之前)
self.last_position = current_position
# 按行处理新内容
lines = new_content.splitlines()
for line in lines:
if '@來自' in line or '@From' in line:
self.logger.info(f"发现新消息: {line[:100]}...")
self.process_trade_message(line)
except Exception as e:
self.logger.error(f"读取日志文件时发生错误: {str(e)}")
def process_trade_message(self, message):
self.logger.info("\n开始处理消息...")
# 首先判断消息类型
if '我想購買' in message or 'would like to buy' in message:
self.logger.info("检测到交易消息")
trade_patterns = [
# 繁体中文格式
{
'pattern': r'.*\[INFO Client \d+\] @來自 ([^:]+): [^]*,我想購買 ([^標]+)標價 ([^在]+)在 [^(]*\(倉庫頁 "([^"]+)"; 位置: ([^)]+)\)',
'format': lambda m: {
'buyer': m.group(1),
'item': m.group(2).strip(),
'price': m.group(3).strip(),
'tab': m.group(4),
'position': m.group(5)
}
},
# 英文格式
{
'pattern': r'.*\[INFO Client \d+\] @From ([^:]+): Hi, I would like to buy your ([^listed]+)listed for ([^in]+)in [^(]*\(stash tab "([^"]+)"; position: ([^)]+)\)',
'format': lambda m: {
'buyer': m.group(1),
'item': m.group(2).strip(),
'price': m.group(3).strip(),
'tab': m.group(4),
'position': m.group(5)
}
}
]
for pattern_info in trade_patterns:
match = re.match(pattern_info['pattern'], message)
if match:
self.logger.info("匹配到交易消息模式")
trade_info = pattern_info['format'](match)
self.logger.info(f"解析的交易信息: {trade_info}")
# 发送交易通知
if TRADE_MESSAGES_CONFIG['message_forward'] and self.sender:
message_content = (
f"买家: {trade_info['buyer']}\n"
f"商品: {trade_info['item']}\n"
f"价格: {trade_info['price']}\n"
f"仓库: {trade_info['tab']}\n"
f"位置: {trade_info['position']}"
)
self.sender.send_message(
content=message_content,
title="POE 交易通知"
)
break
else:
self.logger.info("检测到私聊消息")
# 处理私聊消息
chat_patterns = [
# 繁体中文格式
r'.*\[INFO Client \d+\] @來自 ([^:]+): (.*)',
# 英文格式
r'.*\[INFO Client \d+\] @From ([^:]+): (.*)'
]
for pattern in chat_patterns:
match = re.match(pattern, message)
if match:
sender = match.group(1)
content = match.group(2)
# 发送私聊消息
if CHAT_MESSAGES['message_forward']:
message_content = (
f"发送者:{sender}\n"
f"内容:{content}\n"
f"时间:{time.strftime('%H:%M:%S')}"
)
self.sender.send_message(
content=message_content,
title="POE 私聊消息"
)
break
def monitor_file(handler, is_running):
handler.logger.info("文件监控已启动")
try:
while is_running():
handler.check_new_trades()
time.sleep(0.1)
except Exception as e:
handler.logger.error(f"监控线程异常: {str(e)}")
handler.logger.info("监控线程已停止")
def main():
logger = setup_logging()
logger.info("程序启动...")
# 创建托盘图标
tray = TrayIcon(logger)
logger.info("初始化消息发送器...")
message_sender = MessageSender()
logger.info(f"开始监控日志文件: {CLIENT_LOG_PATH}")
handler = TradeNotificationHandler(message_sender, logger)
# 确保文件存在
if not os.path.exists(CLIENT_LOG_PATH):
logger.error(f"错误:找不到日志文件 {CLIENT_LOG_PATH}")
return
try:
with open(CLIENT_LOG_PATH, 'r', encoding='utf-8', errors='ignore') as f:
f.seek(0, 2)
handler.last_position = f.tell()
logger.info(f"初始文件位置: {handler.last_position}")
except Exception as e:
logger.error(f"读取文件位置时发生错误: {str(e)}")
return
# 在新线程中运行文件监控
monitor_thread = threading.Thread(
target=monitor_file,
args=(handler, lambda: tray.running)
)
monitor_thread.daemon = True
monitor_thread.start()
# 运行托盘图标(这会阻塞主线程)
tray.run()
logger.info("程序已退出")
if __name__ == "__main__":
main()

38
message_sender.py Normal file
View File

@ -0,0 +1,38 @@
import requests
from config import API_KEY, API_URL
class MessageSender:
def __init__(self):
self.api_url = API_URL
self.api_key = API_KEY
def send_message(self, content: str, title: str = "POE 通知") -> bool:
"""
发送消息
:param content: 消息内容
:param title: 消息标题
:return: 是否发送成功
"""
try:
# 完全使用与示例代码相同的结构
response = requests.post(
self.api_url,
headers={
"X-API-Key": self.api_key,
"Content-Type": "application/json"
},
json={
"content": content,
"title": title
}
)
print(f"\n发送消息结果:")
print(f"状态码: {response.status_code}")
print(f"响应: {response.json()}")
return response.status_code == 200
except Exception as e:
print(f"发送消息失败: {str(e)}")
return False

4
requirements.txt Normal file
View File

@ -0,0 +1,4 @@
requests==2.31.0
pyinstaller==6.3.0
pillow==10.2.0
pystray==0.19.5