etsy_manager.py
17 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
"""
Etsy管理模块
处理Etsy相关的所有操作
"""
import json
import time
import random
from datetime import datetime, timedelta
from typing import Dict, List, Optional, Callable
from selenium.webdriver.common.by import By
from .chrome_controller import ChromeController
from ..core.database import Database
from ..core.config import Config
class EtsyManager:
"""Etsy管理器"""
def __init__(self):
self.chrome = ChromeController()
self.database = Database()
self.config = Config()
self.is_marketing_active = False
# API端点
self.API_ENDPOINTS = {
"message_list": "https://www.etsy.com/api/v3/ajax/bespoke/member/conversations/message-list-data",
"tag_counts": "https://www.etsy.com/api/v3/ajax/member/conversations/unread-tag-counts",
"orders": "https://www.etsy.com/api/v3/ajax/bespoke/member/shops/orders"
}
# 页面URL
self.PAGES = {
"messages": "https://www.etsy.com/messages?ref=seller-platform-mcnav",
"orders": "https://www.etsy.com/your/shops/me/orders"
}
def check_login_status(self) -> Dict[str, bool]:
"""检查Etsy登录状态"""
result = {
"logged_in": False,
"message_access": False,
"tag_access": False
}
try:
# 确保浏览器运行
if not self.chrome.ensure_browser_running():
return result
# 导航到消息页面
if not self.chrome.navigate_to(self.PAGES["messages"]):
return result
self.chrome.wait_for_page_load()
# 检查是否需要登录
if "signin" in self.chrome.get_current_url().lower():
print("需要登录Etsy")
return result
# 等待并查找消息列表API响应
message_data = self.chrome.find_api_response(
self.API_ENDPOINTS["message_list"],
timeout=15
)
if message_data:
result["logged_in"] = True
result["message_access"] = True
print("消息API访问正常")
# 检查标签API
tag_data = self.chrome.find_api_response(
self.API_ENDPOINTS["tag_counts"],
timeout=10
)
if tag_data:
result["tag_access"] = True
print("标签API访问正常")
except Exception as e:
print(f"登录状态检查异常: {e}")
return result
def get_user_tags(self) -> Optional[Dict[str, int]]:
"""获取用户标签"""
try:
# 确保浏览器运行
if not self.chrome.ensure_browser_running():
return None
if not self.chrome.navigate_to(self.PAGES["messages"]):
return None
self.chrome.wait_for_page_load()
# 查找标签API响应
tag_data = self.chrome.find_api_response(
self.API_ENDPOINTS["tag_counts"],
timeout=20
)
if tag_data and isinstance(tag_data, dict):
# 提取标签信息
tags = {}
if "tag_counts" in tag_data:
for tag_info in tag_data["tag_counts"]:
if isinstance(tag_info, dict) and "name" in tag_info and "count" in tag_info:
tags[tag_info["name"]] = tag_info["count"]
return tags
except Exception as e:
print(f"获取用户标签异常: {e}")
return None
def start_conversation_marketing(self, start_date: str, end_date: str,
excluded_tags: List[str], status_callback: Callable[[str], None]):
"""启动对话营销"""
self.is_marketing_active = True
sent_count = 0
daily_limit = self.config.get("limits.daily_limit", 100)
try:
# 确保浏览器运行
if not self.chrome.ensure_browser_running():
status_callback("浏览器启动失败")
return
status_callback("获取对话列表...")
# 获取对话列表
conversations = self._get_conversations(start_date, end_date, excluded_tags)
if not conversations:
status_callback("未找到符合条件的对话")
return
status_callback(f"找到 {len(conversations)} 个符合条件的对话")
# 处理对话
for i, conversation in enumerate(conversations):
if not self.is_marketing_active:
status_callback("营销已停止")
break
# 检查每日限制
today_sent = self.database.get_daily_sent_count()
if today_sent >= daily_limit:
status_callback(f"已达每日发送限制 ({daily_limit})")
break
# 概率性休息(发送越多,休息概率越高)
if self.chrome.should_take_break(sent_count):
break_duration = self.chrome.take_random_break()
status_callback(f"已休息 {break_duration/60:.1f} 分钟,继续工作...")
# 处理单个对话
if self._process_conversation(conversation, status_callback):
sent_count += 1
# 智能延迟(基于时间段和长尾分布)
base_delay = self.config.get("limits.base_delay_seconds", 15)
actual_delay = self.chrome.smart_delay(base_delay)
status_callback(f"已等待 {actual_delay:.1f} 秒")
status_callback("对话营销完成")
except Exception as e:
status_callback(f"对话营销异常: {e}")
finally:
self.is_marketing_active = False
def start_order_marketing(self, start_date: str, end_date: str,
excluded_tags: List[str], status_callback: Callable[[str], None]):
"""启动订单营销"""
self.is_marketing_active = True
sent_count = 0
try:
# 确保浏览器运行
if not self.chrome.ensure_browser_running():
status_callback("浏览器启动失败")
return
status_callback("获取订单列表...")
# 获取订单列表
orders = self._get_orders(start_date, end_date)
if not orders:
status_callback("未找到符合条件的订单")
return
status_callback(f"找到 {len(orders)} 个符合条件的订单")
# 处理订单
for order in orders:
if not self.is_marketing_active:
status_callback("营销已停止")
break
# 检查去重
if self.database.is_already_sent(None, order.get("order_id")):
continue
# 处理单个订单
if self._process_order(order, status_callback):
sent_count += 1
# 延迟
delay_range = self.config.get("limits.message_delay_seconds", [10, 20])
delay = random.randint(delay_range[0], delay_range[1])
time.sleep(delay)
status_callback("订单营销完成")
except Exception as e:
status_callback(f"订单营销异常: {e}")
finally:
self.is_marketing_active = False
def stop_marketing(self):
"""停止营销"""
self.is_marketing_active = False
def _get_conversations(self, start_date: str, end_date: str, excluded_tags: List[str]) -> List[Dict]:
"""获取对话列表"""
try:
if not self.chrome.navigate_to(self.PAGES["messages"]):
return []
self.chrome.wait_for_page_load()
# 获取消息列表数据
message_data = self.chrome.find_api_response(
self.API_ENDPOINTS["message_list"],
timeout=20
)
if not message_data or "conversations" not in message_data:
return []
conversations = []
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
for conv in message_data["conversations"]:
# 检查时间范围
last_message_time = conv.get("last_message_time")
if last_message_time:
try:
msg_dt = datetime.fromtimestamp(last_message_time)
if not (start_dt <= msg_dt <= end_dt):
continue
except:
continue
# 检查排除标签
conv_tags = conv.get("tags", [])
if any(tag in excluded_tags for tag in conv_tags):
continue
# 检查是否已发送
conv_id = conv.get("conversation_id")
if conv_id and self.database.is_already_sent(str(conv_id)):
continue
conversations.append(conv)
return conversations
except Exception as e:
print(f"获取对话列表异常: {e}")
return []
def _get_orders(self, start_date: str, end_date: str) -> List[Dict]:
"""获取订单列表"""
try:
if not self.chrome.navigate_to(self.PAGES["orders"]):
return []
self.chrome.wait_for_page_load()
# 获取订单数据
order_data = self.chrome.find_api_response(
self.API_ENDPOINTS["orders"],
timeout=20
)
if not order_data or "orders" not in order_data:
return []
orders = []
start_dt = datetime.strptime(start_date, '%Y-%m-%d')
end_dt = datetime.strptime(end_date, '%Y-%m-%d')
for order in order_data["orders"]:
# 检查时间范围
order_date = order.get("order_date")
if order_date:
try:
order_dt = datetime.fromtimestamp(order_date)
if not (start_dt <= order_dt <= end_dt):
continue
except:
continue
# 检查是否已发送
order_id = order.get("order_id")
if order_id and self.database.is_already_sent(None, str(order_id)):
continue
orders.append(order)
return orders
except Exception as e:
print(f"获取订单列表异常: {e}")
return []
def _process_conversation(self, conversation: Dict, status_callback: Callable[[str], None]) -> bool:
"""处理单个对话"""
try:
conv_id = conversation.get("conversation_id")
customer_name = conversation.get("customer_name", "Unknown")
status_callback(f"处理对话: {customer_name}")
# 打开对话页面
conversation_url = f"https://www.etsy.com/messages/{conv_id}"
if not self.chrome.navigate_to(conversation_url):
return False
self.chrome.wait_for_page_load()
# 模拟阅读历史消息
time.sleep(random.uniform(1, 3))
self.chrome.human_like_scroll()
time.sleep(random.uniform(2, 4)) # 假装阅读内容
# 查找消息输入框 - 基于真实Etsy页面结构的稳健选择器
message_box_selectors = [
"//textarea[@placeholder='Type your reply']", # 最精确:固定placeholder
"//textarea[contains(@class, 'wt-textarea') and contains(@class, 'new-message-textarea-min-height')]", # Etsy设计系统class
"//textarea[contains(@class, 'wt-textarea')]" # 保底:Etsy通用textarea class
]
message_box = None
for selector in message_box_selectors:
message_box = self.chrome.find_element_safe(By.XPATH, selector)
if message_box:
break
if not message_box:
status_callback(f"未找到消息输入框: {customer_name}")
return False
# 选择随机营销消息
messages = self.config.get_marketing_messages()
if not messages:
status_callback("没有配置营销消息")
return False
marketing_message = random.choice(messages)
# 输入消息
if not self.chrome.send_text_safe(message_box, marketing_message):
status_callback(f"消息输入失败: {customer_name}")
return False
# 查找发送按钮 - 基于Etsy设计系统的稳健选择器
send_button_selectors = [
"//button[@type='button' and contains(@class, 'wt-btn--filled') and contains(@class, 'wt-btn--small') and text()='Send']", # 最精确:完整特征组合
"//button[contains(@class, 'wt-btn--filled') and text()='Send']", # 次精确:Etsy主要按钮样式 + 文本
"//button[@type='button' and text()='Send']" # 保底:类型 + 文本
]
send_button = None
for selector in send_button_selectors:
send_button = self.chrome.find_element_safe(By.XPATH, selector)
if send_button:
break
if not send_button:
status_callback(f"未找到发送按钮: {customer_name}")
return False
# 点击发送
if not self.chrome.click_element_safe(send_button):
status_callback(f"发送按钮点击失败: {customer_name}")
return False
# 等待发送完成
time.sleep(2)
# 记录发送历史
self.database.record_sent_message(
conversation_id=str(conv_id),
customer_name=customer_name,
message_content=marketing_message,
trigger_type="conversation",
conversation_start_time=conversation.get("conversation_start_time"),
last_conversation_time=conversation.get("last_message_time"),
tags=conversation.get("tags", [])
)
status_callback(f"✓ 消息已发送: {customer_name}")
return True
except Exception as e:
status_callback(f"处理对话异常: {e}")
return False
def _process_order(self, order: Dict, status_callback: Callable[[str], None]) -> bool:
"""处理单个订单"""
try:
order_id = order.get("order_id")
customer_name = order.get("customer_name", "Unknown")
status_callback(f"处理订单: {order_id} - {customer_name}")
# 这里需要实现从订单页面发起对话的逻辑
# 由于Etsy的订单页面结构比较复杂,这里提供基本框架
# 导航到订单详情页面
order_url = f"https://www.etsy.com/your/shops/me/orders/{order_id}"
if not self.chrome.navigate_to(order_url):
return False
self.chrome.wait_for_page_load()
# 查找"联系买家"或"发送消息"按钮
contact_button_selectors = [
"//button[contains(text(), 'Contact buyer')]",
"//a[contains(text(), 'Send message')]",
"//button[contains(@class, 'contact')]"
]
contact_button = None
for selector in contact_button_selectors:
contact_button = self.chrome.find_element_safe(By.XPATH, selector)
if contact_button:
break
if not contact_button:
status_callback(f"未找到联系买家按钮: {order_id}")
return False
# 点击联系买家
if not self.chrome.click_element_safe(contact_button):
return False
# 等待消息窗口出现
time.sleep(3)
# 继续处理消息发送(类似对话处理逻辑)
# 这里简化处理,实际需要根据页面结构调整
# 记录发送历史
marketing_message = random.choice(self.config.get_marketing_messages())
self.database.record_sent_message(
conversation_id="", # 订单触发可能没有conversation_id
customer_name=customer_name,
order_id=str(order_id),
message_content=marketing_message,
trigger_type="order"
)
status_callback(f"✓ 订单消息已发送: {order_id}")
return True
except Exception as e:
status_callback(f"处理订单异常: {e}")
return False