This commit is contained in:
ddrwode
2026-03-03 10:20:02 +08:00
parent ce371dd31a
commit efb05ae172
3 changed files with 428 additions and 260 deletions

47
add_test_data.py Normal file
View File

@@ -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}')

View File

@@ -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()

View File

@@ -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<els.length;i++){ if(els[i].textContent.trim()==='发送'){ els[i].click(); return true; } } return false;",
]
for script in scripts:
def _wait_history_messages(self, tab) -> 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]