haha
This commit is contained in:
47
add_test_data.py
Normal file
47
add_test_data.py
Normal 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} 条')
|
||||||
146
scripts/seed_contact_data.py
Normal file
146
scripts/seed_contact_data.py
Normal 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()
|
||||||
@@ -1,32 +1,38 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""
|
"""
|
||||||
BOSS 直聘招聘任务处理器。
|
BOSS 直聘招聘任务处理器。
|
||||||
复用并重构原 boss_drission.py 的核心流程。
|
招聘流程与本地脚本 boss_dp/自动化.py 的 main 保持一致:
|
||||||
|
1) 监听并获取 friendList
|
||||||
|
2) 逐个点开会话并监听历史消息
|
||||||
|
3) 若历史消息中未出现“手机号/微信号”,则发送询问并执行“换微信”
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import json
|
||||||
import random
|
import random
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
from typing import Any, Callable, Coroutine, Dict, List
|
from typing import Any, Callable, Coroutine, Dict, List
|
||||||
|
|
||||||
from common.protocol import TaskType
|
from common.protocol import TaskType
|
||||||
from worker.tasks.base import BaseTaskHandler
|
|
||||||
from worker.bit_browser import BitBrowserAPI
|
from worker.bit_browser import BitBrowserAPI
|
||||||
from worker.browser_control import (
|
from worker.browser_control import connect_browser
|
||||||
connect_browser,
|
from worker.tasks.base import BaseTaskHandler
|
||||||
find_element,
|
|
||||||
find_elements,
|
|
||||||
human_click,
|
|
||||||
human_delay,
|
|
||||||
safe_click,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ─── 常量 ───
|
|
||||||
CHAT_INDEX_URL = "https://www.zhipin.com/web/chat/index"
|
CHAT_INDEX_URL = "https://www.zhipin.com/web/chat/index"
|
||||||
ACTION_DELAY = 1.5
|
FRIEND_LIST_API = "wapi/zprelation/friend/getBossFriendListV2"
|
||||||
BETWEEN_CHAT_DELAY = 2.5
|
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):
|
class BossRecruitHandler(BaseTaskHandler):
|
||||||
@@ -44,8 +50,8 @@ class BossRecruitHandler(BaseTaskHandler):
|
|||||||
执行 BOSS 招聘流程。
|
执行 BOSS 招聘流程。
|
||||||
|
|
||||||
params:
|
params:
|
||||||
- job_title: str 招聘岗位名称
|
- job_title: str 招聘岗位名称(用于结果展示)
|
||||||
- max_greet: int 最大打招呼人数(默认 5)
|
- max_greet: int 最大处理会话数(默认 5)
|
||||||
- account_name: str 比特浏览器窗口名(用于打开浏览器)
|
- account_name: str 比特浏览器窗口名(用于打开浏览器)
|
||||||
- account_id: str 比特浏览器窗口 ID(可选,优先级高于 name)
|
- account_id: str 比特浏览器窗口 ID(可选,优先级高于 name)
|
||||||
- bit_api_base: str 比特浏览器 API 地址(可选)
|
- bit_api_base: str 比特浏览器 API 地址(可选)
|
||||||
@@ -58,11 +64,16 @@ class BossRecruitHandler(BaseTaskHandler):
|
|||||||
|
|
||||||
await progress_cb(task_id, "正在打开比特浏览器...")
|
await progress_cb(task_id, "正在打开比特浏览器...")
|
||||||
|
|
||||||
# 在线程池中执行同步的浏览器操作(DrissionPage 是同步库)
|
|
||||||
result = await asyncio.get_event_loop().run_in_executor(
|
result = await asyncio.get_event_loop().run_in_executor(
|
||||||
None,
|
None,
|
||||||
self._run_sync,
|
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
|
return result
|
||||||
|
|
||||||
@@ -77,7 +88,8 @@ class BossRecruitHandler(BaseTaskHandler):
|
|||||||
progress_cb: Callable,
|
progress_cb: Callable,
|
||||||
) -> dict:
|
) -> dict:
|
||||||
"""同步执行浏览器自动化(在线程池中运行)。"""
|
"""同步执行浏览器自动化(在线程池中运行)。"""
|
||||||
# 1. 打开比特浏览器
|
_ = (task_id, progress_cb)
|
||||||
|
|
||||||
bit_api = BitBrowserAPI(bit_api_base)
|
bit_api = BitBrowserAPI(bit_api_base)
|
||||||
addr, port = bit_api.get_browser_for_drission(
|
addr, port = bit_api.get_browser_for_drission(
|
||||||
browser_id=account_id or None,
|
browser_id=account_id or None,
|
||||||
@@ -85,17 +97,10 @@ class BossRecruitHandler(BaseTaskHandler):
|
|||||||
)
|
)
|
||||||
self.logger.info("已打开浏览器, CDP: %s (port=%d)", addr, port)
|
self.logger.info("已打开浏览器, CDP: %s (port=%d)", addr, port)
|
||||||
|
|
||||||
# 2. 连接浏览器
|
|
||||||
browser = connect_browser(port=port)
|
browser = connect_browser(port=port)
|
||||||
tab = browser.latest_tab
|
tab = browser.latest_tab
|
||||||
|
|
||||||
# 3. 打开 BOSS 直聘聊天页
|
flow_result = self._recruit_flow_like_script(tab, job_title, max_greet)
|
||||||
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)
|
|
||||||
collected = flow_result["details"]
|
collected = flow_result["details"]
|
||||||
errors = flow_result["errors"]
|
errors = flow_result["errors"]
|
||||||
|
|
||||||
@@ -118,280 +123,249 @@ class BossRecruitHandler(BaseTaskHandler):
|
|||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def _recruit_flow(self, tab, job_title: str, max_greet: int) -> dict:
|
def _recruit_flow_like_script(self, tab, job_title: str, max_greet: int) -> dict:
|
||||||
"""核心招聘流程:遍历聊天列表,打招呼、询问微信号、收集结果。"""
|
"""按 boss_dp/自动化.py 的 main 流程执行。"""
|
||||||
greeting = f"您好,我们正在招【{job_title}】,看到您的经历比较匹配,方便简单聊聊吗?"
|
collected: List[dict] = []
|
||||||
ask_wechat = "后续沟通会更及时,您方便留一下您的微信号吗?我这边加您。"
|
errors: List[str] = []
|
||||||
collected = []
|
|
||||||
errors = []
|
|
||||||
|
|
||||||
# 获取左侧会话列表
|
try:
|
||||||
items = self._get_conversation_items(tab)
|
friend_list = self._open_chat_and_fetch_friend_list(tab)
|
||||||
if not items:
|
except Exception as e:
|
||||||
self.logger.warning("未找到会话列表元素")
|
err = f"获取 friendList 失败: {e}"
|
||||||
return {"details": collected, "errors": ["未找到会话列表元素"]}
|
self.logger.error(err)
|
||||||
|
return {"details": collected, "errors": [err]}
|
||||||
|
|
||||||
total = min(len(items), max_greet)
|
if not friend_list:
|
||||||
self.logger.info("会话数约 %d,本次处理前 %d 个", len(items), total)
|
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:
|
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)
|
tab.listen.start(HISTORY_API)
|
||||||
if i >= len(items):
|
if not self._open_friend_chat_like_script(tab, name):
|
||||||
break
|
errors.append(f"[{name}] 未找到联系人或点击失败")
|
||||||
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)
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
# 分析聊天上下文
|
messages = self._wait_history_messages(tab)
|
||||||
messages = self._get_chat_messages(tab)
|
has_contact_keyword = self._has_contact_keyword(messages)
|
||||||
ctx = self._analyze_context(messages, job_title)
|
|
||||||
|
|
||||||
# 发招呼
|
action_state = {
|
||||||
if not ctx["already_greeting"]:
|
"asked_wechat": False,
|
||||||
if self._send_message(tab, inp, greeting):
|
"send_success": False,
|
||||||
self.logger.info("[%s] 已发送招呼", name)
|
"exchange_clicked": False,
|
||||||
human_delay(1.5, 2.8)
|
"exchange_confirmed": False,
|
||||||
else:
|
}
|
||||||
self.logger.info("[%s] 已有招呼记录,跳过", name)
|
if not has_contact_keyword:
|
||||||
|
action_state = self._ask_and_exchange_wechat_like_script(tab)
|
||||||
# 与本地自动化.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 "",
|
|
||||||
})
|
|
||||||
|
|
||||||
|
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:
|
except Exception as e:
|
||||||
err_msg = f"处理第 {i + 1} 个会话出错: {e}"
|
err_msg = f"处理第 {i} 个会话出错: {e}"
|
||||||
self.logger.error(err_msg)
|
self.logger.error(err_msg)
|
||||||
errors.append(err_msg)
|
errors.append(err_msg)
|
||||||
continue
|
|
||||||
|
|
||||||
return {"details": collected, "errors": errors}
|
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:
|
@staticmethod
|
||||||
selectors = [
|
def _xpath_literal(value: str) -> str:
|
||||||
"css:div.chat-container div.geek-item",
|
if "'" not in value:
|
||||||
"css:div[role='listitem'] div.geek-item",
|
return f"'{value}'"
|
||||||
"css:.geek-item-wrap .geek-item",
|
if '"' not in value:
|
||||||
"css:div.geek-item",
|
return f'"{value}"'
|
||||||
"css:div[role='listitem']",
|
parts = value.split("'")
|
||||||
]
|
return "concat(" + ", \"'\", ".join(f"'{part}'" for part in parts) + ")"
|
||||||
return find_elements(tab, selectors, timeout=3)
|
|
||||||
|
|
||||||
def _get_candidate_name(self, item, index: int) -> str:
|
def _open_friend_chat_like_script(self, tab, name: str) -> bool:
|
||||||
try:
|
name_selector = f"x://span[text()={self._xpath_literal(name)}]"
|
||||||
name_el = item.ele("css:.geek-name", timeout=1)
|
ele = tab.ele(name_selector, timeout=2)
|
||||||
if name_el and name_el.text:
|
if not ele:
|
||||||
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:
|
|
||||||
return False
|
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:
|
try:
|
||||||
btn = tab.ele(f"x://div[text()=\"{text_send}\"]", timeout=1)
|
ele.run_js("this.scrollIntoView({block: 'center', behavior: 'auto'})")
|
||||||
if btn and safe_click(tab, btn):
|
|
||||||
return True
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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:
|
time.sleep(0.8)
|
||||||
"""
|
|
||||||
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?"
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
human_delay(0.8, 1.6)
|
clickable = tab.ele(name_selector, timeout=2)
|
||||||
btn = tab.ele(f"x://span[text()=\"{text_change}\"]", timeout=1)
|
if not clickable:
|
||||||
if not btn or not safe_click(tab, btn):
|
|
||||||
return False
|
return False
|
||||||
|
clickable.click(by_js=True)
|
||||||
human_delay(0.8, 1.6)
|
return True
|
||||||
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))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _click_send_button(self, tab) -> bool:
|
def _wait_history_messages(self, tab) -> list:
|
||||||
human_delay(0.2, 0.45)
|
packet = tab.listen.wait()
|
||||||
btn = find_element(tab, [
|
body = self._packet_body(packet)
|
||||||
"css:.conversation-editor .submit-content .submit.active",
|
zp_data = body.get("zpData", {}) if isinstance(body, dict) else {}
|
||||||
"css:.conversation-editor div.submit.active",
|
messages = zp_data.get("messages", []) if isinstance(zp_data, dict) else []
|
||||||
"css:div.submit-content div.submit.active",
|
if isinstance(messages, list):
|
||||||
"css:div.submit.active",
|
return messages
|
||||||
"css:.submit-content .submit",
|
return []
|
||||||
"css:div.submit",
|
|
||||||
"text:发送",
|
def _ask_and_exchange_wechat_like_script(self, tab) -> dict:
|
||||||
], timeout=1)
|
state = {
|
||||||
if btn:
|
"asked_wechat": False,
|
||||||
if safe_click(tab, btn):
|
"send_success": False,
|
||||||
return True
|
"exchange_clicked": False,
|
||||||
# JS 兜底
|
"exchange_confirmed": False,
|
||||||
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;",
|
input_box = tab.ele('x://*[@id="boss-chat-editor-input"]', timeout=2)
|
||||||
]
|
if not input_box:
|
||||||
for script in scripts:
|
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:
|
try:
|
||||||
if tab.run_js(script) is True:
|
btn = tab.ele(selector, timeout=1)
|
||||||
|
if btn:
|
||||||
|
btn.click()
|
||||||
return True
|
return True
|
||||||
except Exception:
|
except Exception:
|
||||||
continue
|
continue
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def _get_chat_messages(self, tab) -> List[dict]:
|
def _click_change_wechat_like_script(self, tab) -> bool:
|
||||||
result = []
|
|
||||||
try:
|
try:
|
||||||
items = tab.eles("css:.message-item", timeout=2)
|
btn = tab.ele('x://span[text()="换微信"]', timeout=1)
|
||||||
if not items:
|
if not btn:
|
||||||
return result
|
return False
|
||||||
for e in items[-50:]:
|
btn.click()
|
||||||
t = (e.text or "").strip()
|
return True
|
||||||
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})
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
return False
|
||||||
return result
|
|
||||||
|
|
||||||
def _analyze_context(self, messages: list, job_title: str) -> dict:
|
def _click_exchange_confirm_like_script(self, tab) -> bool:
|
||||||
boss_texts = [m["text"] for m in messages if m.get("role") == "boss"]
|
try:
|
||||||
friend_texts = [m["text"] for m in messages if m.get("role") == "friend"]
|
confirm = tab.ele(EXCHANGE_CONFIRM_XPATH, timeout=2)
|
||||||
full_boss = " ".join(boss_texts)
|
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 = []
|
@staticmethod
|
||||||
phones = []
|
def _packet_body(packet) -> dict:
|
||||||
for t in friend_texts:
|
if not packet:
|
||||||
wechats.extend(self._extract_wechat(t))
|
return {}
|
||||||
phones.extend(self._extract_phone(t))
|
|
||||||
wechats = list(dict.fromkeys(wechats))[:3]
|
response = getattr(packet, "response", None)
|
||||||
phones = list(dict.fromkeys(phones))[:3]
|
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 {
|
return {
|
||||||
"already_greeting": job_title in full_boss or "招" in full_boss,
|
"wechat": wechats[0] if wechats else "",
|
||||||
"already_asked_wechat": "微信" in full_boss or "微信号" in full_boss,
|
"phone": phones[0] if phones else "",
|
||||||
"wechats": wechats,
|
|
||||||
"phones": phones,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_wechat(text: str) -> list:
|
def _extract_wechat(text: str) -> list:
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
found = []
|
found = []
|
||||||
patterns = [
|
patterns = [
|
||||||
r"微信号[::\s]*([a-zA-Z0-9_\-]{6,20})",
|
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"wechat[::\s]*([a-zA-Z0-9_\-]{6,20})",
|
||||||
r"([a-zA-Z][a-zA-Z0-9_\-]{5,19})",
|
r"([a-zA-Z][a-zA-Z0-9_\-]{5,19})",
|
||||||
]
|
]
|
||||||
for p in patterns:
|
|
||||||
for m in re.finditer(p, text, re.IGNORECASE):
|
for pattern in patterns:
|
||||||
s = m.group(1).strip() if m.lastindex else m.group(0).strip()
|
for match in re.finditer(pattern, text, re.IGNORECASE):
|
||||||
if s and s not in found and len(s) >= 6:
|
value = match.group(1).strip() if match.lastindex else match.group(0).strip()
|
||||||
found.append(s)
|
if value and value not in found and len(value) >= 6:
|
||||||
|
found.append(value)
|
||||||
|
|
||||||
return found[:3]
|
return found[:3]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _extract_phone(text: str) -> list:
|
def _extract_phone(text: str) -> list:
|
||||||
"""提取手机号(仅保留 11 位中国大陆手机号)。"""
|
|
||||||
if not text or not text.strip():
|
if not text or not text.strip():
|
||||||
return []
|
return []
|
||||||
|
|
||||||
found = []
|
found = []
|
||||||
# 支持 13800138000 / 138-0013-8000 / 138 0013 8000 等格式
|
|
||||||
raw_candidates = re.findall(r"1[3-9][\d\-\s]{9,15}", text)
|
raw_candidates = re.findall(r"1[3-9][\d\-\s]{9,15}", text)
|
||||||
for raw in raw_candidates:
|
for raw in raw_candidates:
|
||||||
digits = re.sub(r"\D", "", raw)
|
digits = re.sub(r"\D", "", raw)
|
||||||
if len(digits) == 11 and digits.startswith("1") and digits not in found:
|
if len(digits) == 11 and digits.startswith("1") and digits not in found:
|
||||||
found.append(digits)
|
found.append(digits)
|
||||||
|
|
||||||
return found[:3]
|
return found[:3]
|
||||||
|
|||||||
Reference in New Issue
Block a user