This commit is contained in:
ddrwode
2026-03-06 16:09:18 +08:00
parent 9a96e927c3
commit 6cb86ec7fb
3 changed files with 306 additions and 125 deletions

View File

@@ -14,6 +14,8 @@ from server.core.response import api_success
from server.models import ContactRecord, BossAccount, TaskLog from server.models import ContactRecord, BossAccount, TaskLog
from server.core.worker_manager import worker_manager from server.core.worker_manager import worker_manager
GREETED_STATUS = "新打招呼"
@api_view(["GET"]) @api_view(["GET"])
def stats_overview(request): def stats_overview(request):
@@ -32,12 +34,14 @@ def stats_overview(request):
contacts_qs = ContactRecord.objects.filter(date_filter) contacts_qs = ContactRecord.objects.filter(date_filter)
total_contacts = contacts_qs.count() total_contacts = contacts_qs.count()
total_greeted = contacts_qs.filter(reply_status=GREETED_STATUS).count()
total_replied = contacts_qs.filter(reply_status="已回复").count() total_replied = contacts_qs.filter(reply_status="已回复").count()
total_wechat = contacts_qs.filter(wechat_exchanged=True).count() total_wechat = contacts_qs.filter(wechat_exchanged=True).count()
# 今日数据 # 今日数据
today_filter = Q(contacted_at__date=now.date()) today_filter = Q(contacted_at__date=now.date())
today_contacts = ContactRecord.objects.filter(today_filter).count() today_contacts = ContactRecord.objects.filter(today_filter).count()
today_greeted = ContactRecord.objects.filter(today_filter, reply_status=GREETED_STATUS).count()
today_replied = ContactRecord.objects.filter(today_filter, reply_status="已回复").count() today_replied = ContactRecord.objects.filter(today_filter, reply_status="已回复").count()
today_wechat = ContactRecord.objects.filter(today_filter, wechat_exchanged=True).count() today_wechat = ContactRecord.objects.filter(today_filter, wechat_exchanged=True).count()
@@ -60,10 +64,16 @@ def stats_overview(request):
"contacts": { "contacts": {
"total": total_contacts, "total": total_contacts,
"today": today_contacts, "today": today_contacts,
"greeted": total_greeted,
"today_greeted": today_greeted,
"replied": total_replied, "replied": total_replied,
"today_replied": today_replied, "today_replied": today_replied,
"reply_rate": reply_rate, "reply_rate": reply_rate,
}, },
"greetings": {
"total": total_greeted,
"today": today_greeted,
},
"wechat": { "wechat": {
"total": total_wechat, "total": total_wechat,
"today": today_wechat, "today": today_wechat,
@@ -86,7 +96,11 @@ def stats_overview(request):
@api_view(["GET"]) @api_view(["GET"])
def stats_daily(request): def stats_daily(request):
"""按日统计最近 N 天数据。""" """按日统计最近 N 天数据。"""
days = int(request.query_params.get("days", 7)) try:
days = int(request.query_params.get("days", 7))
except (TypeError, ValueError):
days = 7
days = max(1, min(days, 180))
now = timezone.now() now = timezone.now()
daily_data = [] daily_data = []
@@ -95,11 +109,13 @@ def stats_daily(request):
day_filter = Q(contacted_at__date=day) day_filter = Q(contacted_at__date=day)
qs = ContactRecord.objects.filter(day_filter) qs = ContactRecord.objects.filter(day_filter)
total = qs.count() total = qs.count()
greeted = qs.filter(reply_status=GREETED_STATUS).count()
replied = qs.filter(reply_status="已回复").count() replied = qs.filter(reply_status="已回复").count()
wechat = qs.filter(wechat_exchanged=True).count() wechat = qs.filter(wechat_exchanged=True).count()
daily_data.append({ daily_data.append({
"date": f"{day.isoformat()}T00:00:00", "date": f"{day.isoformat()}T00:00:00",
"contacts": total, "contacts": total,
"greeted": greeted,
"replied": replied, "replied": replied,
"wechat": wechat, "wechat": wechat,
"reply_rate": round(replied / max(total, 1) * 100, 1), "reply_rate": round(replied / max(total, 1) * 100, 1),

View File

@@ -26,7 +26,7 @@ from worker.tasks.base import BaseTaskHandler, TaskCancelledError
RECOMMEND_URL = "https://www.zhipin.com/web/chat/recommend" RECOMMEND_URL = "https://www.zhipin.com/web/chat/recommend"
GEEK_LIST_API = "wapi/zpjob/rec/geek/list" GEEK_LIST_API = "wapi/zpjob/rec/geek/list"
FAST_REPLY_TEXT = "您好,我们目前有相关岗位机会,方便了解一下吗?" DEFAULT_GREETING_TEXT = "当前岗位还在招,加个微信了解一下吗?"
# Keep selectors consistent with 1.py logic. # Keep selectors consistent with 1.py logic.
JOB_LIST_SELECTORS = [ JOB_LIST_SELECTORS = [
@@ -63,6 +63,7 @@ class BossRecruitHandler(BaseTaskHandler):
greet_target = self._parse_positive_int(params.get("greet_target"), default=20) greet_target = self._parse_positive_int(params.get("greet_target"), default=20)
position_names = self._normalize_string_list(params.get("position_names")) position_names = self._normalize_string_list(params.get("position_names"))
greeting_messages = self._parse_greeting_messages(params)
# 年龄范围(程序内过滤,不在浏览器筛选中点击) # 年龄范围(程序内过滤,不在浏览器筛选中点击)
age_min = self._parse_optional_int(params.get("age_min")) age_min = self._parse_optional_int(params.get("age_min"))
@@ -76,7 +77,10 @@ class BossRecruitHandler(BaseTaskHandler):
age_desc = f",最小年龄: {age_min}" age_desc = f",最小年龄: {age_min}"
elif age_max is not None: elif age_max is not None:
age_desc = f",最大年龄: {age_max}" age_desc = f",最大年龄: {age_max}"
await progress_cb(task_id, f"正在启动招聘流程,目标打招呼人数: {greet_target}{age_desc}") await progress_cb(
task_id,
f"正在启动招聘流程,目标打招呼人数: {greet_target}{age_desc},启用问候语: {len(greeting_messages)}",
)
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
result = await loop.run_in_executor( result = await loop.run_in_executor(
@@ -87,6 +91,7 @@ class BossRecruitHandler(BaseTaskHandler):
bit_api_base, bit_api_base,
selected_filters, selected_filters,
position_names, position_names,
greeting_messages,
greet_target, greet_target,
worker_id, worker_id,
cancel_event, cancel_event,
@@ -104,6 +109,7 @@ class BossRecruitHandler(BaseTaskHandler):
bit_api_base: str, bit_api_base: str,
selected_filters: List[str], selected_filters: List[str],
position_names: List[str], position_names: List[str],
greeting_messages: List[str],
greet_target: int, greet_target: int,
worker_id: str, worker_id: str,
cancel_event, cancel_event,
@@ -120,6 +126,7 @@ class BossRecruitHandler(BaseTaskHandler):
job_title=job_title, job_title=job_title,
selected_filters=selected_filters, selected_filters=selected_filters,
position_names=position_names, position_names=position_names,
greeting_messages=greeting_messages,
greet_target=greet_target, greet_target=greet_target,
worker_id=worker_id, worker_id=worker_id,
account_name=account_name, account_name=account_name,
@@ -134,6 +141,7 @@ class BossRecruitHandler(BaseTaskHandler):
"actual_greeted": flow.get("actual_greeted", 0), "actual_greeted": flow.get("actual_greeted", 0),
"rounds": flow.get("rounds", 0), "rounds": flow.get("rounds", 0),
"selected_filters": selected_filters, "selected_filters": selected_filters,
"active_greeting_messages": greeting_messages,
"positions": flow.get("positions", []), "positions": flow.get("positions", []),
"details": flow.get("details", []), "details": flow.get("details", []),
"contact_records_created": flow.get("contact_records_created", 0), "contact_records_created": flow.get("contact_records_created", 0),
@@ -164,6 +172,7 @@ class BossRecruitHandler(BaseTaskHandler):
job_title: str, job_title: str,
selected_filters: List[str], selected_filters: List[str],
position_names: List[str], position_names: List[str],
greeting_messages: List[str],
greet_target: int, greet_target: int,
worker_id: str, worker_id: str,
account_name: str, account_name: str,
@@ -231,6 +240,7 @@ class BossRecruitHandler(BaseTaskHandler):
greeted_keys=greeted_keys, greeted_keys=greeted_keys,
position_label=label, position_label=label,
default_job=job_title, default_job=job_title,
greeting_messages=greeting_messages,
cancel_event=cancel_event, cancel_event=cancel_event,
age_min=age_min, age_min=age_min,
age_max=age_max, age_max=age_max,
@@ -259,6 +269,7 @@ class BossRecruitHandler(BaseTaskHandler):
"rounds": round_num, "rounds": round_num,
"positions": [self._position_label(t, v) for t, v in positions], "positions": [self._position_label(t, v) for t, v in positions],
"details": details, "details": details,
"active_greeting_messages": greeting_messages,
"contact_records_created": contact_records_created, "contact_records_created": contact_records_created,
"errors": errors, "errors": errors,
} }
@@ -387,6 +398,7 @@ class BossRecruitHandler(BaseTaskHandler):
greeted_keys: set[str], greeted_keys: set[str],
position_label: str, position_label: str,
default_job: str, default_job: str,
greeting_messages: List[str],
cancel_event, cancel_event,
age_min: Optional[int] = None, age_min: Optional[int] = None,
age_max: Optional[int] = None, age_max: Optional[int] = None,
@@ -409,7 +421,8 @@ class BossRecruitHandler(BaseTaskHandler):
if age_max is not None and age > age_max: if age_max is not None and age > age_max:
continue continue
if not self._greet_one_geek(page, container, item): greeting_text = self._pick_greeting_message(greeting_messages)
if not self._greet_one_geek(page, container, item, greeting_text):
continue continue
greeted_keys.add(geek_key) greeted_keys.add(geek_key)
@@ -423,6 +436,7 @@ class BossRecruitHandler(BaseTaskHandler):
"position": position, "position": position,
"geek_key": geek_key, "geek_key": geek_key,
"source": "new_greet", "source": "new_greet",
"greeting_text": greeting_text,
"greeted_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"), "greeted_at": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
} }
) )
@@ -465,7 +479,7 @@ class BossRecruitHandler(BaseTaskHandler):
except Exception: except Exception:
pass pass
def _greet_one_geek(self, page, container, item: dict) -> bool: def _greet_one_geek(self, page, container, item: dict, greeting_text: str) -> bool:
""" """
对单个牛人打招呼 —— 与 1.py _greet_one_geek 完全一致的逻辑。 对单个牛人打招呼 —— 与 1.py _greet_one_geek 完全一致的逻辑。
使用 page.get_frame(1) 获取详情面板,所有元素查找语法与 1.py 相同。 使用 page.get_frame(1) 获取详情面板,所有元素查找语法与 1.py 相同。
@@ -516,7 +530,8 @@ class BossRecruitHandler(BaseTaskHandler):
time.sleep(random.uniform(0.5, 5)) time.sleep(random.uniform(0.5, 5))
# 与 1.py 一致:快速回复 + 发送 # 与 1.py 一致:快速回复 + 发送
page.ele(f'x://*[@data-placeholder="快速回复"]', timeout=2).input(FAST_REPLY_TEXT) reply_text = str(greeting_text or "").strip() or DEFAULT_GREETING_TEXT
page.ele(f'x://*[@data-placeholder="快速回复"]', timeout=2).input(reply_text)
page.ele(f'x://*[contains(text(),"发送")]', timeout=2).click() page.ele(f'x://*[contains(text(),"发送")]', timeout=2).click()
time.sleep(random.uniform(0.5, 5)) time.sleep(random.uniform(0.5, 5))
@@ -550,6 +565,7 @@ class BossRecruitHandler(BaseTaskHandler):
if not geek_key: if not geek_key:
continue continue
contact_key = f"greet:{geek_key}" contact_key = f"greet:{geek_key}"
greeting_text = str(rec.get("greeting_text", "")).strip()
defaults = { defaults = {
"name": str(rec.get("name", "")).strip() or contact_key, "name": str(rec.get("name", "")).strip() or contact_key,
"position": str(rec.get("position", "")).strip(), "position": str(rec.get("position", "")).strip(),
@@ -557,7 +573,7 @@ class BossRecruitHandler(BaseTaskHandler):
"wechat_exchanged": False, "wechat_exchanged": False,
"worker_id": worker_id, "worker_id": worker_id,
"contacted_at": timezone.now(), "contacted_at": timezone.now(),
"notes": f"新打招呼记录; account={account_name}", "notes": f"新打招呼记录; account={account_name}; greeting={greeting_text or DEFAULT_GREETING_TEXT}",
} }
obj, was_created = ContactRecord.objects.update_or_create( obj, was_created = ContactRecord.objects.update_or_create(
contact=contact_key, contact=contact_key,
@@ -572,6 +588,62 @@ class BossRecruitHandler(BaseTaskHandler):
# ────────────────────── 工具方法 ────────────────────── # ────────────────────── 工具方法 ──────────────────────
def _parse_greeting_messages(self, params: Dict[str, Any]) -> List[str]:
"""
解析并裁剪问候语配置:
- greeting_messages: list / JSON 字符串 / 换行分隔字符串
- greeting_count: 启用前 N 条问候语N>1 时运行期随机发送
"""
raw_messages = params.get("greeting_messages")
if raw_messages is None:
raw_messages = params.get("greetings")
messages: List[str] = []
if isinstance(raw_messages, list):
for item in raw_messages:
text = str(item or "").strip()
if text:
messages.append(text)
elif isinstance(raw_messages, str):
raw = raw_messages.strip()
if raw:
if raw.startswith("[") and raw.endswith("]"):
try:
parsed = json.loads(raw)
if isinstance(parsed, list):
for item in parsed:
text = str(item or "").strip()
if text:
messages.append(text)
except Exception:
pass
if not messages:
for line in raw.replace("\r", "").split("\n"):
text = line.strip()
if text:
messages.append(text)
# 去重并确保至少有系统内置话术
deduped = list(dict.fromkeys(messages))
if not deduped:
deduped = [DEFAULT_GREETING_TEXT]
greeting_count = self._parse_positive_int(
params.get("greeting_count"),
default=min(1, len(deduped)) if deduped else 1,
)
greeting_count = max(1, min(greeting_count, len(deduped)))
return deduped[:greeting_count]
@staticmethod
def _pick_greeting_message(greeting_messages: List[str]) -> str:
pool = [str(item or "").strip() for item in (greeting_messages or []) if str(item or "").strip()]
if not pool:
return DEFAULT_GREETING_TEXT
if len(pool) == 1:
return pool[0]
return random.choice(pool)
@staticmethod @staticmethod
def _normalize_string_list(value: Any) -> List[str]: def _normalize_string_list(value: Any) -> List[str]:
if value is None: if value is None:

View File

@@ -161,40 +161,53 @@ class BossReplyHandler(BaseTaskHandler):
messages = self._wait_history_messages(tab) messages = self._wait_history_messages(tab)
self.ensure_not_cancelled(cancel_event) self.ensure_not_cancelled(cancel_event)
# 过滤掉自己发送的消息 # 过滤掉自己发送的消息,只保留候选人消息
filtered_messages = self._filter_my_messages(messages) filtered_messages = self._filter_my_messages(messages)
has_history_message = bool(filtered_messages)
has_contact_keyword = self._has_contact_keyword(filtered_messages) has_contact_keyword = self._has_contact_keyword(filtered_messages)
contacts = self._extract_contacts(filtered_messages) contacts = self._extract_contacts(filtered_messages)
contact_written = bool(contacts.get("wechat") or contacts.get("phone")) contact_written = bool(contacts.get("wechat") or contacts.get("phone"))
contact_fallback = self._build_friend_contact_fallback(friend, name, friend_job_id)
action_state = { action_state = {
"asked_wechat": False, "asked_wechat": False,
"send_success": False, "send_success": False,
"exchange_clicked": False, "exchange_clicked": False,
"exchange_confirmed": False, "exchange_confirmed": False,
"follow_up_attempted": False,
"follow_up_sent": False,
"follow_up_day": 0,
"follow_up_script_id": 0,
"follow_up_content": "",
"got_reply": False,
"extracted_contact_from_reply": False,
} }
if not has_contact_keyword:
# 先确保存在联系人记录(即使暂时没有微信/手机号,也要入库)
contact_id = self._save_contact_record(
name=name,
job_name=friend_job_name,
contacts=contacts,
action_state=action_state,
contact_fallback=contact_fallback,
)
# 如果候选人此前发过消息优先走复聊配置第1天/第2天/...
if has_history_message and contact_id:
self.ensure_not_cancelled(cancel_event) self.ensure_not_cancelled(cancel_event)
action_state = self._ask_and_exchange_wechat_like_script(tab) follow_state = self._handle_follow_up_chat(tab, name, friend_job_name, contact_id)
action_state.update(follow_state)
# 先保存联系人记录(如果有的话)
temp_contact_id = None # 没有联系方式关键词且未成功发送复聊时,走默认索要微信逻辑
if contacts.get("wechat") or contacts.get("phone"): if not has_contact_keyword and not action_state.get("follow_up_sent", False):
temp_contact_id = self._save_contact_record(name, friend_job_name, contacts, action_state) self.ensure_not_cancelled(cancel_event)
ask_state = self._ask_and_exchange_wechat_like_script(tab)
# 发送后等待对方回复,进行复聊管理 action_state["asked_wechat"] = action_state["asked_wechat"] or ask_state.get("asked_wechat", False)
if action_state["send_success"]: action_state["send_success"] = action_state["send_success"] or ask_state.get("send_success", False)
self.ensure_not_cancelled(cancel_event) action_state["exchange_clicked"] = action_state["exchange_clicked"] or ask_state.get("exchange_clicked", False)
reply_result = self._handle_follow_up_chat(tab, name, friend_job_name, temp_contact_id) action_state["exchange_confirmed"] = (
action_state.update(reply_result) action_state["exchange_confirmed"] or ask_state.get("exchange_confirmed", False)
)
# 如果复聊中提取到了新的联系方式,更新联系人记录
if reply_result.get("extracted_contact_from_reply"):
panel_texts = self._collect_chat_panel_texts(tab)
new_contacts = self._extract_contacts(filtered_messages, extra_texts=panel_texts)
if new_contacts.get("wechat") or new_contacts.get("phone"):
contacts.update(new_contacts)
contact_written = True
panel_texts = self._collect_chat_panel_texts(tab) panel_texts = self._collect_chat_panel_texts(tab)
contacts = self._extract_contacts(filtered_messages, extra_texts=panel_texts) contacts = self._extract_contacts(filtered_messages, extra_texts=panel_texts)
@@ -204,12 +217,16 @@ class BossReplyHandler(BaseTaskHandler):
"[%s] 历史消息含联系方式关键词,但未提取到有效联系方式,疑似识别失败", "[%s] 历史消息含联系方式关键词,但未提取到有效联系方式,疑似识别失败",
name, name,
) )
# 保存联系人记录到数据库获取contact_id用于复聊 # 二次更新联系人记录(补充回复状态/是否交换微信/提取到的新联系方式)
contact_id = None contact_id = self._save_contact_record(
if contact_written: name=name,
contact_id = self._save_contact_record(name, friend_job_name, contacts, action_state) job_name=friend_job_name,
contacts=contacts,
action_state=action_state,
contact_fallback=contact_fallback,
)
collected.append( collected.append(
{ {
"name": name, "name": name,
@@ -218,6 +235,8 @@ class BossReplyHandler(BaseTaskHandler):
"wechat": contacts["wechat"], "wechat": contacts["wechat"],
"phone": contacts["phone"], "phone": contacts["phone"],
"contact_written": contact_written, "contact_written": contact_written,
"contact_recorded": bool(contact_id),
"has_history_message": has_history_message,
"has_contact_keyword": has_contact_keyword, "has_contact_keyword": has_contact_keyword,
**action_state, **action_state,
} }
@@ -248,6 +267,26 @@ class BossReplyHandler(BaseTaskHandler):
return friend_list return friend_list
return [] return []
@staticmethod
def _build_friend_contact_fallback(friend: dict, name: str, job_id: str) -> str:
"""为未提取到微信/手机号的联系人生成稳定占位 contact。"""
candidates = [
friend.get("encryptGeekId"),
friend.get("geekId"),
friend.get("friendId"),
friend.get("uid"),
friend.get("encryptUid"),
friend.get("securityId"),
]
for value in candidates:
normalized = str(value or "").strip()
if normalized:
return f"boss_friend:{normalized}"
base = f"{name}:{job_id}".strip(":")
normalized_base = re.sub(r"\s+", "", base) or name or "unknown"
return f"boss_friend:{normalized_base}"
@staticmethod @staticmethod
def _xpath_literal(value: str) -> str: def _xpath_literal(value: str) -> str:
if "'" not in value: if "'" not in value:
@@ -634,154 +673,208 @@ class BossReplyHandler(BaseTaskHandler):
return filtered return filtered
def _handle_follow_up_chat(self, tab, name: str, job_name: str, contact_id: int = None) -> dict: def _handle_follow_up_chat(self, tab, name: str, job_name: str, contact_id: int = None) -> dict:
"""处理复聊管理,根据配置发送多轮话术""" """处理复聊管理命中第N天话术后发送并尝试交换微信"""
result = { result = {
"send_success": False,
"exchange_clicked": False,
"exchange_confirmed": False,
"follow_up_attempted": False, "follow_up_attempted": False,
"follow_up_sent": False,
"follow_up_day": 0,
"follow_up_script_id": 0,
"follow_up_content": "",
"got_reply": False, "got_reply": False,
"extracted_contact_from_reply": False, "extracted_contact_from_reply": False,
} }
if not contact_id: if not contact_id:
return result return result
try: try:
from server.models import FollowUpConfig, FollowUpScript, FollowUpRecord from server.models import FollowUpConfig, FollowUpScript, FollowUpRecord
from django.utils import timezone from django.utils import timezone
# 获取该岗位的复聊配置
config = FollowUpConfig.objects.filter( config = FollowUpConfig.objects.filter(
position=job_name, position=job_name,
is_active=True is_active=True,
).first() ).first()
if not config: if not config:
# 尝试获取通用配置
config = FollowUpConfig.objects.filter( config = FollowUpConfig.objects.filter(
position="通用", position="通用",
is_active=True is_active=True,
).first() ).first()
if not config: if not config:
self.logger.info("[%s] 未找到复聊配置,跳过复聊", name) self.logger.info("[%s] 未找到复聊配置,跳过复聊", name)
return result return result
# 获取该联系人的复聊记录
last_record = FollowUpRecord.objects.filter( last_record = FollowUpRecord.objects.filter(
contact_id=contact_id, contact_id=contact_id,
config_id=config.id config_id=config.id,
).order_by('-sent_at').first() ).order_by("-sent_at").first()
# 确定当前是第几天
if not last_record: if not last_record:
# 第一次复聊
day_number = 1 day_number = 1
else: else:
# 计算距离上次发送的时间
hours_since_last = (timezone.now() - last_record.sent_at).total_seconds() / 3600 hours_since_last = (timezone.now() - last_record.sent_at).total_seconds() / 3600
# 获取上次使用的话术的间隔时间
last_script = FollowUpScript.objects.filter(id=last_record.script_id).first() last_script = FollowUpScript.objects.filter(id=last_record.script_id).first()
if last_script and hours_since_last < last_script.interval_hours: if last_script and hours_since_last < last_script.interval_hours:
self.logger.info("[%s] 距离上次复聊不足 %d 小时,跳过", name, last_script.interval_hours) self.logger.info("[%s] 距离上次复聊不足 %d 小时,跳过", name, last_script.interval_hours)
return result return result
# 下一天
day_number = last_record.day_number + 1 day_number = last_record.day_number + 1
# 获取该天的话术
script = FollowUpScript.objects.filter( script = FollowUpScript.objects.filter(
config_id=config.id, config_id=config.id,
day_number=day_number, day_number=day_number,
is_active=True is_active=True,
).order_by('order').first() ).order_by("order").first()
# 如果没有该天的话术,尝试获取"往后一直"的话术day_number=0
if not script: if not script:
script = FollowUpScript.objects.filter( script = FollowUpScript.objects.filter(
config_id=config.id, config_id=config.id,
day_number=0, day_number=0,
is_active=True is_active=True,
).order_by('order').first() ).order_by("order").first()
if not script: if not script:
self.logger.info("[%s] 未找到第 %d 天的复聊话术", name, day_number) self.logger.info("[%s] 未找到第 %d 天的复聊话术", name, day_number)
return result return result
# 发送话术
result["follow_up_attempted"] = True result["follow_up_attempted"] = True
result["follow_up_day"] = day_number
result["follow_up_script_id"] = int(script.id)
result["follow_up_content"] = script.content
send_success = self._send_message(tab, script.content) send_success = self._send_message(tab, script.content)
if not send_success:
if send_success: return result
# 记录发送
record = FollowUpRecord.objects.create( result["follow_up_sent"] = True
contact_id=contact_id, result["send_success"] = True
config_id=config.id,
script_id=script.id, record = FollowUpRecord.objects.create(
day_number=day_number, contact_id=contact_id,
content=script.content, config_id=config.id,
) script_id=script.id,
day_number=day_number,
# 等待回复 content=script.content,
reply_info = self._wait_for_reply(tab, script.content) )
if reply_info["got_reply"]:
result["got_reply"] = True time.sleep(random.uniform(0.8, 1.5))
result["extracted_contact_from_reply"] = reply_info["has_contact"] result["exchange_clicked"] = self._click_change_wechat_like_script(tab)
if result["exchange_clicked"]:
# 更新记录 time.sleep(random.uniform(0.8, 1.5))
record.got_reply = True result["exchange_confirmed"] = self._click_exchange_confirm_like_script(tab)
record.reply_content = reply_info["reply_text"]
record.replied_at = timezone.now() reply_info = self._wait_for_reply(tab, script.content)
record.save() if reply_info["got_reply"]:
result["got_reply"] = True
self.logger.info("[%s] 第 %d 天复聊得到回复", name, day_number) result["extracted_contact_from_reply"] = reply_info["has_contact"]
record.got_reply = True
record.reply_content = reply_info["reply_text"]
record.replied_at = timezone.now()
record.save()
self.logger.info("[%s] 第 %d 天复聊得到回复", name, day_number)
except Exception as e: except Exception as e:
self.logger.error("复聊管理失败: %s", e) self.logger.error("复聊管理失败: %s", e)
return result return result
@staticmethod
def _resolve_reply_status(action_state: dict) -> str:
if action_state.get("got_reply"):
return "已回复"
if action_state.get("follow_up_attempted"):
return "复聊中"
if action_state.get("asked_wechat") or action_state.get("send_success"):
return "待回复"
return "未回复"
def _save_contact_record(
def _save_contact_record(self, name: str, job_name: str, contacts: dict, action_state: dict) -> int: self,
"""保存联系人记录到数据库返回contact_id。""" name: str,
job_name: str,
contacts: dict,
action_state: dict,
contact_fallback: str = "",
) -> Optional[int]:
"""保存或更新联系人记录(无联系方式时使用占位 contact"""
try: try:
from server.models import ContactRecord from server.models import ContactRecord
from django.utils import timezone from django.utils import timezone
contact_value = contacts.get("wechat") or contacts.get("phone") or "" actual_contact = str(contacts.get("wechat") or contacts.get("phone") or "").strip()
fallback_contact = str(contact_fallback or "").strip()
contact_value = actual_contact or fallback_contact
if not contact_value: if not contact_value:
return None return None
# 检查是否已存在 existing = None
existing = ContactRecord.objects.filter( if fallback_contact:
name=name, existing = ContactRecord.objects.filter(contact=fallback_contact).first()
contact=contact_value if not existing and actual_contact:
).first() existing = ContactRecord.objects.filter(contact=actual_contact).first()
reply_status = self._resolve_reply_status(action_state)
now = timezone.now()
note_parts = [
"自动回复任务",
f"微信:{contacts.get('wechat', '')}",
f"手机:{contacts.get('phone', '')}",
f"follow_day:{action_state.get('follow_up_day', 0)}",
]
notes = "; ".join(note_parts)
if existing: if existing:
# 更新现有记录 # 已有占位 contact拿到真实联系方式后升级 contact 字段
existing.wechat_exchanged = action_state.get("exchange_confirmed", False) if (
existing.reply_status = "已回复" if action_state.get("got_reply", False) else "未回复" actual_contact
existing.save() and existing.contact != actual_contact
self.logger.info("更新联系人记录: %s - %s", name, contact_value) and not ContactRecord.objects.filter(contact=actual_contact).exclude(pk=existing.pk).exists()
return existing.id ):
else: existing.contact = actual_contact
# 创建新记录 elif not existing.contact:
record = ContactRecord.objects.create( existing.contact = contact_value
name=name,
position=job_name, existing.name = name or existing.name
contact=contact_value, if job_name:
reply_status="已回复" if action_state.get("got_reply", False) else "未回复", existing.position = job_name
wechat_exchanged=action_state.get("exchange_confirmed", False), existing.reply_status = reply_status
contacted_at=timezone.now(), existing.wechat_exchanged = existing.wechat_exchanged or bool(action_state.get("exchange_confirmed"))
notes=f"自动招聘获取 - 微信: {contacts.get('wechat', '')}, 手机: {contacts.get('phone', '')}" existing.contacted_at = now
existing.notes = notes
existing.save(
update_fields=[
"contact",
"name",
"position",
"reply_status",
"wechat_exchanged",
"contacted_at",
"notes",
]
) )
self.logger.info("保存新联系人记录: %s - %s", name, contact_value) self.logger.info("新联系人记录: %s - %s", name, existing.contact)
return record.id return existing.id
record = ContactRecord.objects.create(
name=name,
position=job_name,
contact=contact_value,
reply_status=reply_status,
wechat_exchanged=bool(action_state.get("exchange_confirmed")),
contacted_at=now,
notes=notes,
)
self.logger.info("保存新联系人记录: %s - %s", name, contact_value)
return record.id
except Exception as e: except Exception as e:
self.logger.error("保存联系人记录失败: %s", e) self.logger.error("保存联系人记录失败: %s", e)
return None return None
def _send_message(self, tab, message: str) -> bool: def _send_message(self, tab, message: str) -> bool:
"""发送消息的通用方法。""" """发送消息的通用方法。"""
try: try: