POE2消息转发,初始版本
This commit is contained in:
commit
dd59c2bb00
61
.gitignore
vendored
Normal file
61
.gitignore
vendored
Normal 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
15
build.py
Normal 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
89
config.py
Normal 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
299
main.py
Normal 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
38
message_sender.py
Normal 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
4
requirements.txt
Normal file
@ -0,0 +1,4 @@
|
||||
requests==2.31.0
|
||||
pyinstaller==6.3.0
|
||||
pillow==10.2.0
|
||||
pystray==0.19.5
|
Loading…
Reference in New Issue
Block a user