diff --git a/add_test_data.py b/add_test_data.py new file mode 100644 index 0000000..fc567d5 --- /dev/null +++ b/add_test_data.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import sys +import django +from datetime import datetime, timedelta + +os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings') +sys.path.insert(0, '.') + +django.setup() + +from server.models import ContactRecord + +test_data = [ + {'name': '张三', 'position': 'Python 开发工程师', 'contact': '13800138001', 'reply_status': '已回复', 'wechat_exchanged': True, 'worker_id': 'worker_001', 'account_id': 1, 'notes': '积极主动,回复迅速', 'contacted_at': datetime.now() - timedelta(days=1)}, + {'name': '李四', 'position': '前端开发工程师', 'contact': 'wx_li_si_2023', 'reply_status': '已回复', 'wechat_exchanged': True, 'worker_id': 'worker_001', 'account_id': 1, 'notes': '已交换微信,约好周五面试', 'contacted_at': datetime.now() - timedelta(days=2)}, + {'name': '王五', 'position': 'Java 开发工程师', 'contact': '13900139001', 'reply_status': '未回复', 'wechat_exchanged': False, 'worker_id': 'worker_002', 'notes': '待跟进', 'contacted_at': datetime.now() - timedelta(days=3)}, + {'name': '赵六', 'position': '数据分析师', 'contact': 'wx_zhao_liu', 'reply_status': '已回复', 'wechat_exchanged': False, 'worker_id': 'worker_002', 'account_id': 2, 'notes': '表示有兴趣,但尚未交换微信', 'contacted_at': datetime.now() - timedelta(days=4)}, + {'name': '孙七', 'position': '产品经理', 'contact': '13600136001', 'reply_status': '已回复', 'wechat_exchanged': True, 'worker_id': 'worker_001', 'account_id': 1, 'notes': '已交换微信,线索质量高', 'contacted_at': datetime.now() - timedelta(days=5)}, + {'name': '周八', 'position': '设计师', 'contact': 'wx_zhou_ba_design', 'reply_status': '已回复', 'wechat_exchanged': True, 'worker_id': 'worker_003', 'account_id': 3, 'notes': '已交换微信,待进一步沟通', 'contacted_at': datetime.now() - timedelta(days=6)}, + {'name': '吴九', 'position': '后端开发工程师', 'contact': '18800188001', 'reply_status': '未回复', 'wechat_exchanged': False, 'worker_id': 'worker_002', 'notes': '一小时前才联系', 'contacted_at': datetime.now() - timedelta(hours=1)}, + {'name': '郑十', 'position': '运维工程师', 'contact': 'wx_zheng_shi_ops', 'reply_status': '已回复', 'wechat_exchanged': False, 'worker_id': 'worker_001', 'account_id': 1, 'notes': '表示最近比较忙,下周可联系', 'contacted_at': datetime.now() - timedelta(days=2)}, +] + +print("⏳ 正在添加测试数据...\n") +created_count = 0 +for item in test_data: + existing = ContactRecord.objects.filter(name=item['name'], position=item['position']).exists() + if not existing: + ContactRecord(**item).save() + created_count += 1 + print(f'✓ 创建: {item["name"]} - {item["contact"]}') + else: + print(f'⊘ 跳过: {item["name"]} (已存在)') + +total = ContactRecord.objects.count() +wechat = ContactRecord.objects.filter(contact__icontains='wx').count() +phone = total - wechat +exchanged = ContactRecord.objects.filter(wechat_exchanged=True).count() + +print(f'\n✅ 完成!') +print(f' 本次新增: {created_count} 条') +print(f' 总记录数: {total} 条') +print(f' 微信号: {wechat} 条') +print(f' 电话号: {phone} 条') +print(f' 已交换微信: {exchanged} 条') diff --git a/scripts/seed_contact_data.py b/scripts/seed_contact_data.py new file mode 100644 index 0000000..c4c2353 --- /dev/null +++ b/scripts/seed_contact_data.py @@ -0,0 +1,146 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +为 ContactRecord 表添加测试数据的脚本。 +用法: python manage.py shell < scripts/seed_contact_data.py +或 django-admin shell --settings=server.settings < scripts/seed_contact_data.py +""" + +import os +import sys +import django +from datetime import datetime, timedelta + +# 配置 Django 环境 +os.environ.setdefault("DJANGO_SETTINGS_MODULE", "server.settings") +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +django.setup() + +from server.models import ContactRecord + +# 测试数据集合:包含微信号和电话号码 +test_data = [ + { + "name": "张三", + "position": "Python 开发工程师", + "contact": "13800138001", # 电话号码 + "reply_status": "已回复", + "wechat_exchanged": True, + "worker_id": "worker_001", + "account_id": 1, + "notes": "积极主动,回复迅速", + "contacted_at": datetime.now() - timedelta(days=1), + }, + { + "name": "李四", + "position": "前端开发工程师", + "contact": "wx_li_si_2023", # 微信号 + "reply_status": "已回复", + "wechat_exchanged": True, + "worker_id": "worker_001", + "account_id": 1, + "notes": "已交换微信,约好周五面试", + "contacted_at": datetime.now() - timedelta(days=2), + }, + { + "name": "王五", + "position": "Java 开发工程师", + "contact": "13900139001", # 电话号码 + "reply_status": "未回复", + "wechat_exchanged": False, + "worker_id": "worker_002", + "notes": "待跟进", + "contacted_at": datetime.now() - timedelta(days=3), + }, + { + "name": "赵六", + "position": "数据分析师", + "contact": "wx_zhao_liu", # 微信号 + "reply_status": "已回复", + "wechat_exchanged": False, + "worker_id": "worker_002", + "account_id": 2, + "notes": "表示有兴趣,但尚未交换微信", + "contacted_at": datetime.now() - timedelta(days=4), + }, + { + "name": "孙七", + "position": "产品经理", + "contact": "13600136001", # 电话号码 + "reply_status": "已回复", + "wechat_exchanged": True, + "worker_id": "worker_001", + "account_id": 1, + "notes": "已交换微信,线索质量高", + "contacted_at": datetime.now() - timedelta(days=5), + }, + { + "name": "周八", + "position": "设计师", + "contact": "wx_zhou_ba_design", # 微信号 + "reply_status": "已回复", + "wechat_exchanged": True, + "worker_id": "worker_003", + "account_id": 3, + "notes": "已交换微信,待进一步沟通", + "contacted_at": datetime.now() - timedelta(days=6), + }, + { + "name": "吴九", + "position": "后端开发工程师", + "contact": "18800188001", # 电话号码 + "reply_status": "未回复", + "wechat_exchanged": False, + "worker_id": "worker_002", + "notes": "一小时前才联系", + "contacted_at": datetime.now() - timedelta(hours=1), + }, + { + "name": "郑十", + "position": "运维工程师", + "contact": "wx_zheng_shi_ops", # 微信号 + "reply_status": "已回复", + "wechat_exchanged": False, + "worker_id": "worker_001", + "account_id": 1, + "notes": "表示最近比较忙,下周可联系", + "contacted_at": datetime.now() - timedelta(days=2), + }, +] + +def seed_data(): + """插入测试数据到数据库""" + print("⏳ 正在添加测试数据...") + + created_count = 0 + for item in test_data: + # 检查该联系人是否已存在(基于名字和岗位) + existing = ContactRecord.objects.filter( + name=item["name"], + position=item["position"] + ).exists() + + if not existing: + contact = ContactRecord(**item) + contact.save() + created_count += 1 + print(f"✓ 创建: {item['name']} ({item['position']}) - {item['contact']}") + else: + print(f"⊘ 跳过: {item['name']} (已存在)") + + # 统计信息 + total_contacts = ContactRecord.objects.count() + wechat_count = ContactRecord.objects.filter(contact__contains="wx").count() + phone_count = total_contacts - wechat_count + exchanged_count = ContactRecord.objects.filter(wechat_exchanged=True).count() + + print(f"\n✅ 添加完成!") + print(f" 本次新增: {created_count} 条记录") + print(f" 总记录数: {total_contacts}") + print(f" 微信号数: {wechat_count}") + print(f" 电话号数: {phone_count}") + print(f" 已交换微信: {exchanged_count}") + +if __name__ == "__main__": + seed_data() diff --git a/worker/tasks/boss_recruit.py b/worker/tasks/boss_recruit.py index 11526c7..5fb1f08 100644 --- a/worker/tasks/boss_recruit.py +++ b/worker/tasks/boss_recruit.py @@ -1,32 +1,38 @@ # -*- coding: utf-8 -*- """ BOSS 直聘招聘任务处理器。 -复用并重构原 boss_drission.py 的核心流程。 +招聘流程与本地脚本 boss_dp/自动化.py 的 main 保持一致: +1) 监听并获取 friendList +2) 逐个点开会话并监听历史消息 +3) 若历史消息中未出现“手机号/微信号”,则发送询问并执行“换微信” """ from __future__ import annotations import asyncio +import json import random import re import time from typing import Any, Callable, Coroutine, Dict, List from common.protocol import TaskType -from worker.tasks.base import BaseTaskHandler from worker.bit_browser import BitBrowserAPI -from worker.browser_control import ( - connect_browser, - find_element, - find_elements, - human_click, - human_delay, - safe_click, -) +from worker.browser_control import connect_browser +from worker.tasks.base import BaseTaskHandler -# ─── 常量 ─── CHAT_INDEX_URL = "https://www.zhipin.com/web/chat/index" -ACTION_DELAY = 1.5 -BETWEEN_CHAT_DELAY = 2.5 +FRIEND_LIST_API = "wapi/zprelation/friend/getBossFriendListV2" +HISTORY_API = "wapi/zpchat/boss/historyMsg" +ASK_WECHAT_TEXT = "后续沟通会更及时,您方便留一下您的微信号吗?我这边加您。" +CONTACT_KEYWORDS = ("手机号", "微信号") +EXCHANGE_CONFIRM_XPATH = ( + "x://span[contains(text(),'确定与对方交换微信吗?')]/../div[@class='btn-box']/" + "span[contains(@class,'boss-btn-primary')]" +) +EXCHANGE_CONFIRM_XPATH_ASCII = ( + "x://span[contains(text(),'确定与对方交换微信吗?')]/../div[@class='btn-box']/" + "span[contains(@class,'boss-btn-primary')]" +) class BossRecruitHandler(BaseTaskHandler): @@ -44,8 +50,8 @@ class BossRecruitHandler(BaseTaskHandler): 执行 BOSS 招聘流程。 params: - - job_title: str 招聘岗位名称 - - max_greet: int 最大打招呼人数(默认 5) + - job_title: str 招聘岗位名称(用于结果展示) + - max_greet: int 最大处理会话数(默认 5) - account_name: str 比特浏览器窗口名(用于打开浏览器) - account_id: str 比特浏览器窗口 ID(可选,优先级高于 name) - bit_api_base: str 比特浏览器 API 地址(可选) @@ -58,11 +64,16 @@ class BossRecruitHandler(BaseTaskHandler): await progress_cb(task_id, "正在打开比特浏览器...") - # 在线程池中执行同步的浏览器操作(DrissionPage 是同步库) result = await asyncio.get_event_loop().run_in_executor( None, self._run_sync, - task_id, job_title, max_greet, account_name, account_id, bit_api_base, progress_cb, + task_id, + job_title, + max_greet, + account_name, + account_id, + bit_api_base, + progress_cb, ) return result @@ -77,7 +88,8 @@ class BossRecruitHandler(BaseTaskHandler): progress_cb: Callable, ) -> dict: """同步执行浏览器自动化(在线程池中运行)。""" - # 1. 打开比特浏览器 + _ = (task_id, progress_cb) + bit_api = BitBrowserAPI(bit_api_base) addr, port = bit_api.get_browser_for_drission( browser_id=account_id or None, @@ -85,17 +97,10 @@ class BossRecruitHandler(BaseTaskHandler): ) self.logger.info("已打开浏览器, CDP: %s (port=%d)", addr, port) - # 2. 连接浏览器 browser = connect_browser(port=port) tab = browser.latest_tab - # 3. 打开 BOSS 直聘聊天页 - tab.get(CHAT_INDEX_URL) - tab.wait.load_start() - human_delay(2.5, 4.0) - - # 4. 执行招聘流程 - flow_result = self._recruit_flow(tab, job_title, max_greet) + flow_result = self._recruit_flow_like_script(tab, job_title, max_greet) collected = flow_result["details"] errors = flow_result["errors"] @@ -118,280 +123,249 @@ class BossRecruitHandler(BaseTaskHandler): return result - def _recruit_flow(self, tab, job_title: str, max_greet: int) -> dict: - """核心招聘流程:遍历聊天列表,打招呼、询问微信号、收集结果。""" - greeting = f"您好,我们正在招【{job_title}】,看到您的经历比较匹配,方便简单聊聊吗?" - ask_wechat = "后续沟通会更及时,您方便留一下您的微信号吗?我这边加您。" - collected = [] - errors = [] + def _recruit_flow_like_script(self, tab, job_title: str, max_greet: int) -> dict: + """按 boss_dp/自动化.py 的 main 流程执行。""" + collected: List[dict] = [] + errors: List[str] = [] - # 获取左侧会话列表 - items = self._get_conversation_items(tab) - if not items: - self.logger.warning("未找到会话列表元素") - return {"details": collected, "errors": ["未找到会话列表元素"]} + try: + friend_list = self._open_chat_and_fetch_friend_list(tab) + except Exception as e: + err = f"获取 friendList 失败: {e}" + self.logger.error(err) + return {"details": collected, "errors": [err]} - total = min(len(items), max_greet) - self.logger.info("会话数约 %d,本次处理前 %d 个", len(items), total) + if not friend_list: + return {"details": collected, "errors": ["未拿到 friendList"]} - for i in range(total): + total = len(friend_list) + if isinstance(max_greet, int) and max_greet > 0: + total = min(total, max_greet) + + self.logger.info("friendList 总数=%d,本次处理=%d", len(friend_list), total) + + for i, friend in enumerate(friend_list[:total], start=1): try: - human_delay(max(1.8, BETWEEN_CHAT_DELAY - 0.7), BETWEEN_CHAT_DELAY + 1.0) + name = str(friend.get("name", "")).strip() or f"候选人{i}" + friend_job_name = str(friend.get("jobName", "")).strip() + friend_job_id = str(friend.get("jobId", "")).strip() - items = self._get_conversation_items(tab) - if i >= len(items): - break - item = items[i] - - # 获取候选人名称 - name = self._get_candidate_name(item, i) - - # 点击进入聊天 - self._click_conversation(tab, item) - human_delay(1.2, 2.2) - - # 等待输入框 - inp = self._wait_for_input(tab) - if not inp: - self.logger.info("[%s] 未进入聊天,跳过", name) + tab.listen.start(HISTORY_API) + if not self._open_friend_chat_like_script(tab, name): + errors.append(f"[{name}] 未找到联系人或点击失败") continue - # 分析聊天上下文 - messages = self._get_chat_messages(tab) - ctx = self._analyze_context(messages, job_title) + messages = self._wait_history_messages(tab) + has_contact_keyword = self._has_contact_keyword(messages) - # 发招呼 - if not ctx["already_greeting"]: - if self._send_message(tab, inp, greeting): - self.logger.info("[%s] 已发送招呼", name) - human_delay(1.5, 2.8) - else: - self.logger.info("[%s] 已有招呼记录,跳过", name) - - # 与本地自动化.py一致:没有联系方式时,发送询问并触发“换微信” - if not ctx["wechats"] and not ctx["phones"]: - if self._send_message(tab, inp, ask_wechat): - self.logger.info("[%s] 已询问微信号", name) - human_delay(1.2, 2.2) - if self._try_exchange_wechat(tab): - self.logger.info("[%s] 已执行换微信", name) - human_delay(1.2, 2.2) - - # 收集微信号 - human_delay(1.0, 2.0) - messages = self._get_chat_messages(tab) - ctx = self._analyze_context(messages, job_title) - wechats = ctx["wechats"][:2] - phones = ctx["phones"][:2] - - collected.append({ - "name": name, - "job": job_title, - "wechat": wechats[0] if wechats else "", - "phone": phones[0] if phones else "", - }) + action_state = { + "asked_wechat": False, + "send_success": False, + "exchange_clicked": False, + "exchange_confirmed": False, + } + if not has_contact_keyword: + action_state = self._ask_and_exchange_wechat_like_script(tab) + contacts = self._extract_contacts(messages) + collected.append( + { + "name": name, + "job": friend_job_name or job_title, + "job_id": friend_job_id, + "wechat": contacts["wechat"], + "phone": contacts["phone"], + "has_contact_keyword": has_contact_keyword, + **action_state, + } + ) except Exception as e: - err_msg = f"处理第 {i + 1} 个会话出错: {e}" + err_msg = f"处理第 {i} 个会话出错: {e}" self.logger.error(err_msg) errors.append(err_msg) - continue return {"details": collected, "errors": errors} - # ─── 辅助方法 ─── + def _open_chat_and_fetch_friend_list(self, tab) -> list: + tab.listen.start(FRIEND_LIST_API) + tab.get(CHAT_INDEX_URL) + packet = tab.listen.wait() + body = self._packet_body(packet) + zp_data = body.get("zpData", {}) if isinstance(body, dict) else {} + friend_list = zp_data.get("friendList", []) if isinstance(zp_data, dict) else [] + if isinstance(friend_list, list): + return friend_list + return [] - def _get_conversation_items(self, tab) -> list: - selectors = [ - "css:div.chat-container div.geek-item", - "css:div[role='listitem'] div.geek-item", - "css:.geek-item-wrap .geek-item", - "css:div.geek-item", - "css:div[role='listitem']", - ] - return find_elements(tab, selectors, timeout=3) + @staticmethod + def _xpath_literal(value: str) -> str: + if "'" not in value: + return f"'{value}'" + if '"' not in value: + return f'"{value}"' + parts = value.split("'") + return "concat(" + ", \"'\", ".join(f"'{part}'" for part in parts) + ")" - def _get_candidate_name(self, item, index: int) -> str: - try: - name_el = item.ele("css:.geek-name", timeout=1) - if name_el and name_el.text: - return name_el.text.strip() - except Exception: - pass - try: - if item.text: - return item.text.strip()[:20] - except Exception: - pass - return f"候选人{index + 1}" - - def _click_conversation(self, tab, item) -> bool: - if safe_click(tab, item): - return True - try: - parent = item.parent() - if parent and parent.attr("role") == "listitem": - return safe_click(tab, parent) - except Exception: - pass - return False - - def _wait_for_input(self, tab, retries: int = 6): - for _ in range(retries): - inp = find_element(tab, [ - "css:#boss-chat-editor-input", - "css:.boss-chat-editor-input", - ], timeout=1) - if inp: - return inp - human_delay(0.5, 0.9) - return None - - def _send_message(self, tab, inp, message: str) -> bool: - try: - human_click(tab, inp) - human_delay(0.25, 0.55) - try: - inp.clear() - except Exception: - pass - human_delay(0.15, 0.4) - inp.input(message) - human_delay(0.4, 0.9) - if self._click_send_button_like_script(tab): - return True - if self._click_send_button(tab): - return True - inp.input("\n") - human_delay(0.35, 0.7) - return True - except Exception: + def _open_friend_chat_like_script(self, tab, name: str) -> bool: + name_selector = f"x://span[text()={self._xpath_literal(name)}]" + ele = tab.ele(name_selector, timeout=2) + if not ele: return False - def _click_send_button_like_script(self, tab) -> bool: - """Use the same selector style as 自动化.py for the send button.""" - text_send = "\u53d1\u9001" try: - btn = tab.ele(f"x://div[text()=\"{text_send}\"]", timeout=1) - if btn and safe_click(tab, btn): - return True + ele.run_js("this.scrollIntoView({block: 'center', behavior: 'auto'})") except Exception: pass - try: - btn = tab.ele(f"x://span[text()=\"{text_send}\"]", timeout=1) - if btn and safe_click(tab, btn): - return True - except Exception: - pass - return False - def _try_exchange_wechat(self, tab) -> bool: - """ - Click '换微信', then confirm the '确定与对方交换微信吗?' dialog. - """ - text_change = "\u6362\u5fae\u4fe1" - tip_cn = "\u786e\u5b9a\u4e0e\u5bf9\u65b9\u4ea4\u6362\u5fae\u4fe1\u5417\uff1f" - tip_en = "\u786e\u5b9a\u4e0e\u5bf9\u65b9\u4ea4\u6362\u5fae\u4fe1\u5417?" + time.sleep(0.8) try: - human_delay(0.8, 1.6) - btn = tab.ele(f"x://span[text()=\"{text_change}\"]", timeout=1) - if not btn or not safe_click(tab, btn): + clickable = tab.ele(name_selector, timeout=2) + if not clickable: return False - - human_delay(0.8, 1.6) - confirm = tab.ele( - f"x://span[contains(text(),\"{tip_cn}\")]/../div[@class=\"btn-box\"]/span[contains(@class,\"boss-btn-primary\")]", - timeout=2, - ) - if not confirm: - confirm = tab.ele( - f"x://span[contains(text(),\"{tip_en}\")]/../div[@class=\"btn-box\"]/span[contains(@class,\"boss-btn-primary\")]", - timeout=2, - ) - return bool(confirm and safe_click(tab, confirm)) + clickable.click(by_js=True) + return True except Exception: return False - def _click_send_button(self, tab) -> bool: - human_delay(0.2, 0.45) - btn = find_element(tab, [ - "css:.conversation-editor .submit-content .submit.active", - "css:.conversation-editor div.submit.active", - "css:div.submit-content div.submit.active", - "css:div.submit.active", - "css:.submit-content .submit", - "css:div.submit", - "text:发送", - ], timeout=1) - if btn: - if safe_click(tab, btn): - return True - # JS 兜底 - scripts = [ - "var el = document.querySelector('.conversation-editor .submit.active') || document.querySelector('.conversation-editor .submit'); if(el){ el.click(); return true; } return false;", - "var els = document.querySelectorAll('div[class*=\"submit\"]'); for(var i=0;i list: + packet = tab.listen.wait() + body = self._packet_body(packet) + zp_data = body.get("zpData", {}) if isinstance(body, dict) else {} + messages = zp_data.get("messages", []) if isinstance(zp_data, dict) else [] + if isinstance(messages, list): + return messages + return [] + + def _ask_and_exchange_wechat_like_script(self, tab) -> dict: + state = { + "asked_wechat": False, + "send_success": False, + "exchange_clicked": False, + "exchange_confirmed": False, + } + + input_box = tab.ele('x://*[@id="boss-chat-editor-input"]', timeout=2) + if not input_box: + return state + + input_box.input(ASK_WECHAT_TEXT) + state["asked_wechat"] = True + + time.sleep(random.randint(1, 3) + random.random()) + state["send_success"] = self._click_send_like_script(tab) + + time.sleep(random.randint(1, 5) + random.random()) + state["exchange_clicked"] = self._click_change_wechat_like_script(tab) + + if state["exchange_clicked"]: + time.sleep(random.randint(1, 2) + random.random()) + state["exchange_confirmed"] = self._click_exchange_confirm_like_script(tab) + + return state + + def _click_send_like_script(self, tab) -> bool: + for selector in ('x://div[text()="发送"]', 'x://span[text()="发送"]'): try: - if tab.run_js(script) is True: + btn = tab.ele(selector, timeout=1) + if btn: + btn.click() return True except Exception: continue return False - def _get_chat_messages(self, tab) -> List[dict]: - result = [] + def _click_change_wechat_like_script(self, tab) -> bool: try: - items = tab.eles("css:.message-item", timeout=2) - if not items: - return result - for e in items[-50:]: - t = (e.text or "").strip() - if not t or "沟通的职位" in t: - continue - role = "friend" - try: - if e.ele("css:.item-boss", timeout=0): - role = "boss" - elif e.ele("css:.item-friend", timeout=0): - role = "friend" - else: - cls = (e.attr("class") or "") + " " - if "item-boss" in cls or ("boss" in cls and "friend" not in cls): - role = "boss" - except Exception: - if any(k in t for k in ("招", "岗位", "微信号", "方便留", "加您")): - role = "boss" - result.append({"role": role, "text": t}) + btn = tab.ele('x://span[text()="换微信"]', timeout=1) + if not btn: + return False + btn.click() + return True except Exception: - pass - return result + return False - def _analyze_context(self, messages: list, job_title: str) -> dict: - boss_texts = [m["text"] for m in messages if m.get("role") == "boss"] - friend_texts = [m["text"] for m in messages if m.get("role") == "friend"] - full_boss = " ".join(boss_texts) + def _click_exchange_confirm_like_script(self, tab) -> bool: + try: + confirm = tab.ele(EXCHANGE_CONFIRM_XPATH, timeout=2) + if not confirm: + confirm = tab.ele(EXCHANGE_CONFIRM_XPATH_ASCII, timeout=2) + if not confirm: + return False + confirm.click(by_js=True) + return True + except Exception: + return False - wechats = [] - phones = [] - for t in friend_texts: - wechats.extend(self._extract_wechat(t)) - phones.extend(self._extract_phone(t)) - wechats = list(dict.fromkeys(wechats))[:3] - phones = list(dict.fromkeys(phones))[:3] + @staticmethod + def _packet_body(packet) -> dict: + if not packet: + return {} + + response = getattr(packet, "response", None) + body = getattr(response, "body", None) if response is not None else None + + if isinstance(body, dict): + return body + + if isinstance(body, str): + try: + parsed = json.loads(body) + if isinstance(parsed, dict): + return parsed + except Exception: + return {} + + return {} + + def _history_texts(self, messages: list) -> list[str]: + texts: list[str] = [] + for msg in messages: + if not isinstance(msg, dict): + continue + + body = msg.get("body") + text = "" + if isinstance(body, dict): + maybe_text = body.get("text") + if isinstance(maybe_text, str): + text = maybe_text + elif isinstance(msg.get("text"), str): + text = str(msg.get("text")) + + text = text.strip() + if text: + texts.append(text) + + return texts + + def _has_contact_keyword(self, messages: list) -> bool: + for text in self._history_texts(messages): + if any(k in text for k in CONTACT_KEYWORDS): + return True + return False + + def _extract_contacts(self, messages: list) -> dict: + wechats: list[str] = [] + phones: list[str] = [] + + for text in self._history_texts(messages): + wechats.extend(self._extract_wechat(text)) + phones.extend(self._extract_phone(text)) + + wechats = list(dict.fromkeys(wechats)) + phones = list(dict.fromkeys(phones)) return { - "already_greeting": job_title in full_boss or "招" in full_boss, - "already_asked_wechat": "微信" in full_boss or "微信号" in full_boss, - "wechats": wechats, - "phones": phones, + "wechat": wechats[0] if wechats else "", + "phone": phones[0] if phones else "", } @staticmethod def _extract_wechat(text: str) -> list: if not text or not text.strip(): return [] + found = [] patterns = [ r"微信号[::\s]*([a-zA-Z0-9_\-]{6,20})", @@ -400,24 +374,25 @@ class BossRecruitHandler(BaseTaskHandler): r"wechat[::\s]*([a-zA-Z0-9_\-]{6,20})", r"([a-zA-Z][a-zA-Z0-9_\-]{5,19})", ] - for p in patterns: - for m in re.finditer(p, text, re.IGNORECASE): - s = m.group(1).strip() if m.lastindex else m.group(0).strip() - if s and s not in found and len(s) >= 6: - found.append(s) + + for pattern in patterns: + for match in re.finditer(pattern, text, re.IGNORECASE): + value = match.group(1).strip() if match.lastindex else match.group(0).strip() + if value and value not in found and len(value) >= 6: + found.append(value) + return found[:3] @staticmethod def _extract_phone(text: str) -> list: - """提取手机号(仅保留 11 位中国大陆手机号)。""" if not text or not text.strip(): return [] found = [] - # 支持 13800138000 / 138-0013-8000 / 138 0013 8000 等格式 raw_candidates = re.findall(r"1[3-9][\d\-\s]{9,15}", text) for raw in raw_candidates: digits = re.sub(r"\D", "", raw) if len(digits) == 11 and digits.startswith("1") and digits not in found: found.append(digits) + return found[:3]