haha
This commit is contained in:
139
1.py
139
1.py
@@ -11,6 +11,7 @@ import sys
|
||||
import time
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
# 保证从项目根目录运行时可导入 worker 包
|
||||
_ROOT = Path(__file__).resolve().parent
|
||||
@@ -50,16 +51,23 @@ def _connect_local_chrome():
|
||||
return page
|
||||
|
||||
|
||||
def _connect_bit_browser():
|
||||
def _connect_bit_browser(
|
||||
*,
|
||||
bit_api_base: Optional[str] = None,
|
||||
browser_name: Optional[str] = None,
|
||||
browser_id: Optional[str] = None,
|
||||
):
|
||||
"""通过比特浏览器 API 打开并连接,返回 ChromiumPage。"""
|
||||
from worker.bit_browser import BitBrowserAPI
|
||||
from DrissionPage import ChromiumPage, ChromiumOptions
|
||||
|
||||
print("正在连接比特浏览器 API...")
|
||||
bit_api = BitBrowserAPI(BIT_API_BASE)
|
||||
bit_api = BitBrowserAPI(bit_api_base or BIT_API_BASE)
|
||||
print("正在打开比特浏览器...")
|
||||
cdp_addr, port, browser_id = bit_api.open_browser(
|
||||
browser_id=BROWSER_ID, name=BROWSER_NAME, remark=None
|
||||
browser_id=browser_id if browser_id is not None else BROWSER_ID,
|
||||
name=browser_name if browser_name is not None else BROWSER_NAME,
|
||||
remark=None
|
||||
)
|
||||
print(f"已打开浏览器 ID={browser_id}, CDP 端口={port}")
|
||||
co = ChromiumOptions().set_local_port(port=port)
|
||||
@@ -166,6 +174,83 @@ def _greet_geek_list_skip_greeted(page, container, geek_list, greeted_keys):
|
||||
return n
|
||||
|
||||
|
||||
def _dedupe_filter_values(items: List[str]) -> List[str]:
|
||||
seen = set()
|
||||
result: List[str] = []
|
||||
for item in items:
|
||||
value = str(item or "").strip()
|
||||
if not value or value in seen:
|
||||
continue
|
||||
seen.add(value)
|
||||
result.append(value)
|
||||
return result
|
||||
|
||||
|
||||
def _parse_recommend_filters(raw_filters: List[dict]) -> Dict[str, Any]:
|
||||
groups: List[dict] = []
|
||||
flat_options: List[str] = []
|
||||
display_filters: Dict[str, Any] = {}
|
||||
|
||||
for index, item in enumerate(raw_filters or []):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
|
||||
name = str(item.get("name", "")).strip()
|
||||
if not name:
|
||||
continue
|
||||
|
||||
options: List[str] = []
|
||||
for option in item.get("options") or []:
|
||||
if not isinstance(option, dict):
|
||||
continue
|
||||
option_name = str(option.get("name", "")).strip()
|
||||
if option_name:
|
||||
options.append(option_name)
|
||||
options = _dedupe_filter_values(options)
|
||||
|
||||
group: Dict[str, Any] = {
|
||||
"name": name,
|
||||
"order": index,
|
||||
"options": options,
|
||||
}
|
||||
|
||||
start = item.get("start")
|
||||
end = item.get("end")
|
||||
try:
|
||||
if start is not None and end is not None:
|
||||
range_payload = {"start": int(start), "end": int(end)}
|
||||
group["range"] = range_payload
|
||||
display_filters[name] = range_payload
|
||||
else:
|
||||
display_filters[name] = options
|
||||
except Exception:
|
||||
display_filters[name] = options
|
||||
|
||||
groups.append(group)
|
||||
flat_options.extend(options)
|
||||
|
||||
return {
|
||||
"groups": groups,
|
||||
"flat_options": _dedupe_filter_values(flat_options),
|
||||
"display_filters": display_filters,
|
||||
"raw_payload": {"filters": raw_filters or []},
|
||||
}
|
||||
|
||||
|
||||
def fetch_recommend_filters(page, timeout_sec: int = 30) -> Dict[str, Any]:
|
||||
"""抓取推荐页筛选项并返回结构化数据。"""
|
||||
page.listen.start('wapi/zpblock/recommend/filters')
|
||||
page.get("https://www.zhipin.com/web/chat/recommend")
|
||||
res = page.listen.wait(timeout=timeout_sec)
|
||||
body = getattr(getattr(res, "response", None), "body", None) or {}
|
||||
zp_data = body.get("zpData", {}) if isinstance(body, dict) else {}
|
||||
vip_filter = zp_data.get("vipFilter", {}) if isinstance(zp_data, dict) else {}
|
||||
raw_filters = vip_filter.get("filters", []) if isinstance(vip_filter, dict) else []
|
||||
if not isinstance(raw_filters, list):
|
||||
raw_filters = []
|
||||
return _parse_recommend_filters(raw_filters)
|
||||
|
||||
|
||||
def main(filters, position_names=None, greet_target=None):
|
||||
"""
|
||||
推荐牛人流程:多岗位循环,所有岗位合计达到目标人数即停;不够则从第一个岗位再跑一轮直到够了。
|
||||
@@ -256,36 +341,30 @@ def main(filters, position_names=None, greet_target=None):
|
||||
time.sleep(1)
|
||||
|
||||
|
||||
def main1():
|
||||
if USE_LOCAL_CHROME:
|
||||
def main1(
|
||||
*,
|
||||
use_local_chrome: Optional[bool] = None,
|
||||
bit_api_base: Optional[str] = None,
|
||||
browser_name: Optional[str] = None,
|
||||
browser_id: Optional[str] = None,
|
||||
timeout_sec: int = 30,
|
||||
echo: bool = True,
|
||||
):
|
||||
use_local = USE_LOCAL_CHROME if use_local_chrome is None else use_local_chrome
|
||||
|
||||
if use_local:
|
||||
page = _connect_local_chrome()
|
||||
else:
|
||||
page = _connect_bit_browser()
|
||||
page = _connect_bit_browser(
|
||||
bit_api_base=bit_api_base,
|
||||
browser_name=browser_name,
|
||||
browser_id=browser_id,
|
||||
)
|
||||
|
||||
page.listen.start('wapi/zpblock/recommend/filters')
|
||||
|
||||
# 示例:打开一个页面(可选)
|
||||
page.get("https://www.zhipin.com/web/chat/recommend")
|
||||
res = page.listen.wait()
|
||||
|
||||
filters = {}
|
||||
for i in res.response.body["zpData"]["vipFilter"]["filters"]:
|
||||
print(i)
|
||||
|
||||
if i["name"] == "年龄":
|
||||
print(i["start"])
|
||||
print(i["end"])
|
||||
|
||||
filters[i["name"]] = range(int(i["start"]), int(i["end"]) + 1)
|
||||
else:
|
||||
datas = []
|
||||
for i1 in i["options"]:
|
||||
print(i1["name"])
|
||||
datas.append(i1["name"])
|
||||
|
||||
filters[i["name"]] = datas
|
||||
|
||||
print(filters)
|
||||
payload = fetch_recommend_filters(page, timeout_sec=timeout_sec)
|
||||
if echo:
|
||||
print(payload["display_filters"])
|
||||
return payload
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -11,6 +11,8 @@ DEFAULTS = {
|
||||
"server_url": "ws://8.137.99.82:9000/ws",
|
||||
"worker_id": "worker-1",
|
||||
"worker_name": "本机",
|
||||
"sync_browser_id": "",
|
||||
"sync_browser_name": "",
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
from typing import Optional
|
||||
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication,
|
||||
@@ -25,6 +26,7 @@ from PyQt5.QtWidgets import (
|
||||
QPushButton,
|
||||
QTextEdit,
|
||||
QProgressBar,
|
||||
QComboBox,
|
||||
QMessageBox,
|
||||
QFrame,
|
||||
)
|
||||
@@ -32,6 +34,7 @@ from PyQt5.QtCore import Qt, QTimer, pyqtSignal, QObject, QEvent
|
||||
from PyQt5.QtGui import QFont, QTextCursor
|
||||
|
||||
from .config_store import load_config, save_config, get_project_root
|
||||
from worker.bit_browser import BitBrowserAPI
|
||||
|
||||
# 日志缓冲
|
||||
LOG_BATCH_CHARS = 2048
|
||||
@@ -39,6 +42,41 @@ LOG_MAX_LINES = 2000
|
||||
LOG_DRAIN_INTERVAL_MS = 120
|
||||
|
||||
|
||||
def _browser_online_flag(browser: dict) -> Optional[bool]:
|
||||
for key in ("isRunning", "isOpen", "open", "is_open"):
|
||||
if key in browser:
|
||||
return bool(browser.get(key))
|
||||
|
||||
status = str(browser.get("status", "")).strip().lower()
|
||||
if not status:
|
||||
return None
|
||||
if status in {"1", "true", "online", "running", "open", "已打开", "打开", "在线"}:
|
||||
return True
|
||||
if status in {"0", "false", "offline", "stopped", "closed", "已关闭", "关闭", "离线"}:
|
||||
return False
|
||||
return None
|
||||
|
||||
|
||||
def fetch_browser_profiles() -> list[dict]:
|
||||
"""从本地比特浏览器读取可选环境;若能识别在线状态则优先返回在线环境。"""
|
||||
bit_api = BitBrowserAPI()
|
||||
items = bit_api.list_browsers(page_size=200)
|
||||
|
||||
profiles = []
|
||||
for item in items:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
profiles.append({
|
||||
"id": str(item.get("id", "") or "").strip(),
|
||||
"name": str(item.get("name", "") or "").strip(),
|
||||
"remark": str(item.get("remark", "") or "").strip(),
|
||||
"online": _browser_online_flag(item),
|
||||
})
|
||||
|
||||
online_profiles = [item for item in profiles if item.get("online") is True]
|
||||
return online_profiles or profiles
|
||||
|
||||
|
||||
def do_git_pull(project_root: str, log_callback) -> bool:
|
||||
"""在项目目录执行 git pull。"""
|
||||
git = shutil.which("git")
|
||||
@@ -81,11 +119,23 @@ def _get_worker_launcher() -> tuple[str, str, bool]:
|
||||
return sys.executable, project_root, False
|
||||
|
||||
|
||||
def run_worker(server_url: str, worker_id: str, worker_name: str, log_callback) -> subprocess.Popen | None:
|
||||
def run_worker(
|
||||
server_url: str,
|
||||
worker_id: str,
|
||||
worker_name: str,
|
||||
log_callback,
|
||||
sync_browser_id: str = "",
|
||||
sync_browser_name: str = "",
|
||||
) -> subprocess.Popen | None:
|
||||
"""启动 Worker 子进程(同一 exe 的 --worker 模式或 python -m worker.main)。"""
|
||||
launcher, cwd, use_worker_flag = _get_worker_launcher()
|
||||
if use_worker_flag:
|
||||
cmd = [launcher, "--worker", "--server", server_url, "--worker-id", worker_id, "--worker-name", worker_name]
|
||||
cmd = [
|
||||
launcher, "--worker",
|
||||
"--server", server_url,
|
||||
"--worker-id", worker_id,
|
||||
"--worker-name", worker_name,
|
||||
]
|
||||
env = os.environ
|
||||
encoding = "gbk" if sys.platform == "win32" else "utf-8"
|
||||
else:
|
||||
@@ -97,6 +147,10 @@ def run_worker(server_url: str, worker_id: str, worker_name: str, log_callback)
|
||||
]
|
||||
env = {**os.environ, "PYTHONIOENCODING": "utf-8"}
|
||||
encoding = "utf-8"
|
||||
if sync_browser_id:
|
||||
cmd.extend(["--sync-browser-id", sync_browser_id])
|
||||
if sync_browser_name:
|
||||
cmd.extend(["--sync-browser-name", sync_browser_name])
|
||||
try:
|
||||
return subprocess.Popen(
|
||||
cmd, cwd=cwd, env=env,
|
||||
@@ -159,6 +213,7 @@ class ClientGUI(QMainWindow):
|
||||
self._build_ui()
|
||||
self._log_worker = LogWorker(self._log_queue, self)
|
||||
self._log_worker.batch_ready.connect(self._append_log_batch)
|
||||
QTimer.singleShot(0, self._on_refresh_browsers)
|
||||
|
||||
def _build_ui(self):
|
||||
central = QWidget()
|
||||
@@ -185,6 +240,19 @@ class ClientGUI(QMainWindow):
|
||||
self.entry_worker_name.setText(self.config.get("worker_name", ""))
|
||||
cfg_layout.addWidget(self.entry_worker_name, 2, 1)
|
||||
|
||||
cfg_layout.addWidget(QLabel("同步环境:"), 3, 0)
|
||||
browser_layout = QHBoxLayout()
|
||||
self.combo_sync_browser = QComboBox()
|
||||
self.combo_sync_browser.setMinimumWidth(280)
|
||||
browser_layout.addWidget(self.combo_sync_browser, 1)
|
||||
|
||||
self.btn_refresh_browsers = QPushButton("刷新环境")
|
||||
self.btn_refresh_browsers.setCursor(Qt.PointingHandCursor)
|
||||
self.btn_refresh_browsers.clicked.connect(lambda _checked=False: self._on_refresh_browsers(notify=True))
|
||||
browser_layout.addWidget(self.btn_refresh_browsers)
|
||||
cfg_layout.addLayout(browser_layout, 3, 1)
|
||||
self._set_browser_placeholder("正在加载浏览器环境…")
|
||||
|
||||
layout.addWidget(cfg_group)
|
||||
|
||||
# 操作按钮
|
||||
@@ -242,6 +310,9 @@ class ClientGUI(QMainWindow):
|
||||
self.config["server_url"] = self.entry_server.text().strip()
|
||||
self.config["worker_id"] = self.entry_worker_id.text().strip()
|
||||
self.config["worker_name"] = self.entry_worker_name.text().strip()
|
||||
browser = self._selected_browser()
|
||||
self.config["sync_browser_id"] = browser.get("id", "")
|
||||
self.config["sync_browser_name"] = browser.get("name", "")
|
||||
save_config(self.config)
|
||||
|
||||
def _log(self, msg: str):
|
||||
@@ -295,17 +366,94 @@ class ClientGUI(QMainWindow):
|
||||
else:
|
||||
QMessageBox.warning(self, "更新", "更新失败,请检查网络或 Git 配置")
|
||||
return True
|
||||
if isinstance(e, _BrowserProfilesEvent):
|
||||
self.btn_refresh_browsers.setEnabled(True)
|
||||
self._apply_browser_profiles(e.profiles)
|
||||
if e.error:
|
||||
self._log(f"读取浏览器环境失败: {e.error}\n")
|
||||
if e.notify:
|
||||
QMessageBox.warning(self, "浏览器环境", e.error)
|
||||
elif e.notify:
|
||||
self._log(f"已加载 {len(e.profiles)} 个浏览器环境。\n")
|
||||
return True
|
||||
return super().event(e)
|
||||
|
||||
def _set_browser_placeholder(self, text: str):
|
||||
self.combo_sync_browser.clear()
|
||||
self.combo_sync_browser.addItem(text, None)
|
||||
|
||||
def _selected_browser(self) -> dict:
|
||||
data = self.combo_sync_browser.currentData()
|
||||
return data if isinstance(data, dict) else {}
|
||||
|
||||
def _browser_label(self, browser: dict) -> str:
|
||||
name = browser.get("name", "") or "(未命名环境)"
|
||||
remark = browser.get("remark", "")
|
||||
return f"{name} [{remark}]" if remark else name
|
||||
|
||||
def _apply_browser_profiles(self, profiles: list[dict]):
|
||||
self.combo_sync_browser.clear()
|
||||
if not profiles:
|
||||
self._set_browser_placeholder("未找到可用浏览器环境")
|
||||
return
|
||||
|
||||
for browser in profiles:
|
||||
self.combo_sync_browser.addItem(self._browser_label(browser), browser)
|
||||
|
||||
saved_id = str(self.config.get("sync_browser_id", "") or "").strip()
|
||||
saved_name = str(self.config.get("sync_browser_name", "") or "").strip()
|
||||
target_index = 0
|
||||
for index in range(self.combo_sync_browser.count()):
|
||||
browser = self.combo_sync_browser.itemData(index)
|
||||
if not isinstance(browser, dict):
|
||||
continue
|
||||
if saved_id and browser.get("id") == saved_id:
|
||||
target_index = index
|
||||
break
|
||||
if not saved_id and saved_name and browser.get("name") == saved_name:
|
||||
target_index = index
|
||||
break
|
||||
self.combo_sync_browser.setCurrentIndex(target_index)
|
||||
|
||||
def _on_refresh_browsers(self, notify: bool = False):
|
||||
self.btn_refresh_browsers.setEnabled(False)
|
||||
self._set_browser_placeholder("正在加载浏览器环境…")
|
||||
if notify:
|
||||
self._log("正在刷新浏览器环境列表...\n")
|
||||
|
||||
def task():
|
||||
try:
|
||||
profiles = fetch_browser_profiles()
|
||||
error = ""
|
||||
except Exception as err:
|
||||
profiles = []
|
||||
error = str(err)
|
||||
QApplication.instance().postEvent(self, _BrowserProfilesEvent(profiles, error, notify))
|
||||
|
||||
threading.Thread(target=task, daemon=True).start()
|
||||
|
||||
def _on_start(self):
|
||||
self._save_config()
|
||||
server_url = self.entry_server.text().strip() or "ws://8.137.99.82:9000/ws"
|
||||
worker_id = self.entry_worker_id.text().strip() or "worker-1"
|
||||
worker_name = self.entry_worker_name.text().strip() or "本机"
|
||||
browser = self._selected_browser()
|
||||
sync_browser_id = str(browser.get("id", "") or "").strip()
|
||||
sync_browser_name = str(browser.get("name", "") or "").strip()
|
||||
|
||||
self._set_status("启动中…", "#3b82f6")
|
||||
if sync_browser_name:
|
||||
self._log(f"正在启动 Worker,并同步筛选环境:{sync_browser_name}\n")
|
||||
else:
|
||||
self._log("正在启动 Worker...\n")
|
||||
proc = run_worker(server_url, worker_id, worker_name, self._log)
|
||||
proc = run_worker(
|
||||
server_url,
|
||||
worker_id,
|
||||
worker_name,
|
||||
self._log,
|
||||
sync_browser_id=sync_browser_id,
|
||||
sync_browser_name=sync_browser_name,
|
||||
)
|
||||
if proc is None:
|
||||
self._set_status("就绪", "#22c55e")
|
||||
return
|
||||
@@ -367,6 +515,16 @@ class _FinishUpdateEvent(QEvent):
|
||||
self.ok = ok
|
||||
|
||||
|
||||
class _BrowserProfilesEvent(QEvent):
|
||||
Type = QEvent.Type(QEvent.registerEventType())
|
||||
|
||||
def __init__(self, profiles: list[dict], error: str = "", notify: bool = False):
|
||||
super().__init__(_BrowserProfilesEvent.Type)
|
||||
self.profiles = profiles
|
||||
self.error = error
|
||||
self.notify = notify
|
||||
|
||||
|
||||
def main():
|
||||
app = QApplication(sys.argv)
|
||||
w = ClientGUI()
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
数据库初始化脚本:创建测试数据
|
||||
用于测试新增的筛选和复聊功能
|
||||
用于测试招聘回复与复聊功能
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -13,36 +13,7 @@ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')
|
||||
django.setup()
|
||||
|
||||
from server.models import FilterConfig, ChatScript
|
||||
|
||||
|
||||
def create_filter_config():
|
||||
"""创建筛选配置示例"""
|
||||
print("创建筛选配置...")
|
||||
|
||||
# 删除旧的测试配置
|
||||
FilterConfig.objects.filter(name__contains="测试").delete()
|
||||
|
||||
# 创建新配置
|
||||
config = FilterConfig.objects.create(
|
||||
name="Python开发筛选配置",
|
||||
age_min=22,
|
||||
age_max=35,
|
||||
gender="不限",
|
||||
education="本科",
|
||||
activity="3天内活跃",
|
||||
positions=["Python开发", "后端开发", "全栈开发", "Django开发"],
|
||||
greeting_min=5,
|
||||
greeting_max=20,
|
||||
rest_minutes=30,
|
||||
collection_min=10,
|
||||
collection_max=50,
|
||||
message_interval=30,
|
||||
is_active=True
|
||||
)
|
||||
print(f"✓ 创建筛选配置: {config.name} (ID: {config.id})")
|
||||
|
||||
return config
|
||||
from server.models import ChatScript
|
||||
|
||||
|
||||
def create_chat_scripts():
|
||||
@@ -106,21 +77,12 @@ def main():
|
||||
print("=" * 60)
|
||||
|
||||
try:
|
||||
# 创建筛选配置
|
||||
config = create_filter_config()
|
||||
|
||||
# 创建话术
|
||||
scripts = create_chat_scripts()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("初始化完成!")
|
||||
print("=" * 60)
|
||||
print(f"\n筛选配置: {config.name}")
|
||||
print(f" - 年龄: {config.age_min}-{config.age_max}岁")
|
||||
print(f" - 学历: {config.education}及以上")
|
||||
print(f" - 活跃度: {config.activity}")
|
||||
print(f" - 期望职位: {', '.join(config.positions)}")
|
||||
|
||||
print(f"\n话术配置: 共 {len(scripts)} 条")
|
||||
for script in scripts:
|
||||
print(f" - {script.position} / {script.get_script_type_display()}")
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
功能验证脚本:测试新增的筛选和消息过滤功能
|
||||
功能验证脚本:测试消息过滤与联系方式提取功能
|
||||
"""
|
||||
|
||||
import os
|
||||
@@ -132,33 +132,6 @@ def test_contact_extraction():
|
||||
print(f" [-] 未提取到联系方式")
|
||||
|
||||
|
||||
# 测试学历筛选
|
||||
def test_education_filter():
|
||||
print("\n" + "=" * 60)
|
||||
print("测试学历筛选功能")
|
||||
print("=" * 60)
|
||||
|
||||
edu_levels = ["初中", "高中", "中专", "大专", "本科", "硕士", "博士"]
|
||||
|
||||
test_cases = [
|
||||
("本科", "大专", False), # 要求本科,候选人大专,不通过
|
||||
("本科", "本科", True), # 要求本科,候选人本科,通过
|
||||
("本科", "硕士", True), # 要求本科,候选人硕士,通过
|
||||
("大专", "本科", True), # 要求大专,候选人本科,通过
|
||||
("硕士", "本科", False), # 要求硕士,候选人本科,不通过
|
||||
]
|
||||
|
||||
for required, candidate, expected in test_cases:
|
||||
candidate_level = next((i for i, edu in enumerate(edu_levels) if edu in candidate), -1)
|
||||
required_level = next((i for i, edu in enumerate(edu_levels) if edu in required), -1)
|
||||
|
||||
result = candidate_level >= required_level if candidate_level != -1 and required_level != -1 else True
|
||||
status = "[OK] 通过" if result == expected else "[FAIL] 失败"
|
||||
|
||||
print(f"\n要求: {required}, 候选人: {candidate}")
|
||||
print(f" 期望: {'通过' if expected else '不通过'}, 实际: {'通过' if result else '不通过'} {status}")
|
||||
|
||||
|
||||
def main():
|
||||
print("\n" + "=" * 60)
|
||||
print("BOSS招聘自动化 - 功能验证")
|
||||
@@ -168,7 +141,6 @@ def main():
|
||||
test_time_parsing()
|
||||
test_message_filtering()
|
||||
test_contact_extraction()
|
||||
test_education_filter()
|
||||
|
||||
print("\n" + "=" * 60)
|
||||
print("所有测试完成!")
|
||||
|
||||
@@ -1,52 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Filter APIs:
|
||||
- CRUD for FilterConfig (legacy/manual configs)
|
||||
- Recruit filter options (synced from site at worker startup)
|
||||
"""
|
||||
from rest_framework import status
|
||||
from rest_framework.decorators import api_view
|
||||
|
||||
from server.core.response import api_error, api_success
|
||||
from server.models import BossAccount, FilterConfig, RecruitFilterSnapshot
|
||||
from server.models import BossAccount, RecruitFilterSnapshot
|
||||
from server.serializers import (
|
||||
FilterConfigSerializer,
|
||||
RecruitFilterSnapshotSerializer,
|
||||
)
|
||||
|
||||
|
||||
@api_view(["GET", "POST"])
|
||||
def filter_list(request):
|
||||
if request.method == "GET":
|
||||
qs = FilterConfig.objects.all().order_by("-updated_at")
|
||||
return api_success(FilterConfigSerializer(qs, many=True).data)
|
||||
|
||||
ser = FilterConfigSerializer(data=request.data)
|
||||
ser.is_valid(raise_exception=True)
|
||||
ser.save()
|
||||
return api_success(ser.data, http_status=status.HTTP_201_CREATED)
|
||||
|
||||
|
||||
@api_view(["GET", "PUT", "DELETE"])
|
||||
def filter_detail(request, pk):
|
||||
try:
|
||||
obj = FilterConfig.objects.get(pk=pk)
|
||||
except FilterConfig.DoesNotExist:
|
||||
return api_error(status.HTTP_404_NOT_FOUND, "筛选配置不存在")
|
||||
|
||||
if request.method == "GET":
|
||||
return api_success(FilterConfigSerializer(obj).data)
|
||||
|
||||
if request.method == "PUT":
|
||||
ser = FilterConfigSerializer(obj, data=request.data, partial=True)
|
||||
ser.is_valid(raise_exception=True)
|
||||
ser.save()
|
||||
return api_success(ser.data)
|
||||
|
||||
obj.delete()
|
||||
return api_success(msg="筛选配置已删除")
|
||||
|
||||
|
||||
def _snapshot_payload(snapshot: RecruitFilterSnapshot) -> dict:
|
||||
data = RecruitFilterSnapshotSerializer(snapshot).data
|
||||
return {
|
||||
@@ -102,4 +68,3 @@ def recruit_filter_options(request):
|
||||
}
|
||||
)
|
||||
return api_success(_snapshot_payload(snapshot))
|
||||
|
||||
|
||||
16
server/migrations/0008_delete_filterconfig.py
Normal file
16
server/migrations/0008_delete_filterconfig.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Generated by Codex on 2026-03-06
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("server", "0007_recruitfiltersnapshot"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.DeleteModel(
|
||||
name="FilterConfig",
|
||||
),
|
||||
]
|
||||
35
server/migrations/0009_task.py
Normal file
35
server/migrations/0009_task.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# Generated by Django 6.0.2 on 2026-03-06 10:45
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('server', '0008_delete_filterconfig'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Task',
|
||||
fields=[
|
||||
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
|
||||
('task_id', models.CharField(max_length=32, unique=True, verbose_name='任务 ID')),
|
||||
('task_type', models.CharField(max_length=64, verbose_name='任务类型')),
|
||||
('worker_id', models.CharField(default='', max_length=64, verbose_name='执行的 Worker')),
|
||||
('account_name', models.CharField(blank=True, default='', max_length=128, verbose_name='账号(环境名称)')),
|
||||
('status', models.CharField(default='', max_length=32, verbose_name='当前状态')),
|
||||
('params', models.JSONField(blank=True, null=True, verbose_name='任务参数')),
|
||||
('progress', models.TextField(blank=True, null=True, verbose_name='进度信息')),
|
||||
('result', models.JSONField(blank=True, null=True, verbose_name='任务结果')),
|
||||
('error', models.TextField(blank=True, null=True, verbose_name='错误信息')),
|
||||
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')),
|
||||
('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')),
|
||||
],
|
||||
options={
|
||||
'verbose_name': '任务',
|
||||
'verbose_name_plural': '任务',
|
||||
'db_table': 'task',
|
||||
},
|
||||
),
|
||||
]
|
||||
@@ -105,42 +105,6 @@ class AuthToken(models.Model):
|
||||
return self.username
|
||||
|
||||
|
||||
class FilterConfig(models.Model):
|
||||
"""筛选条件配置表。"""
|
||||
name = models.CharField(max_length=128, verbose_name="配置名称")
|
||||
position_keywords = models.CharField(max_length=512, default="", blank=True, verbose_name="岗位关键词列表")
|
||||
city = models.CharField(max_length=64, default="", blank=True, verbose_name="城市")
|
||||
salary_min = models.CharField(max_length=32, default="", blank=True, verbose_name="最低薪资(K)")
|
||||
salary_max = models.CharField(max_length=32, default="", blank=True, verbose_name="最高薪资(K)")
|
||||
experience = models.CharField(max_length=64, default="", blank=True, verbose_name="工作经验")
|
||||
education = models.CharField(max_length=32, default="不限", verbose_name="学历要求")
|
||||
is_active = models.BooleanField(default=True, verbose_name="是否启用")
|
||||
# 以下为兼容旧版保留字段
|
||||
age_min = models.IntegerField(default=18, verbose_name="最小年龄")
|
||||
age_max = models.IntegerField(default=60, verbose_name="最大年龄")
|
||||
gender = models.CharField(max_length=32, default="不限", verbose_name="性别")
|
||||
activity = models.CharField(max_length=32, default="不限", verbose_name="活跃度")
|
||||
positions = models.JSONField(default=list, blank=True, verbose_name="期望岗位列表")
|
||||
greeting_min = models.IntegerField(default=5, verbose_name="打招呼最少条数/天")
|
||||
greeting_max = models.IntegerField(default=20, verbose_name="打招呼最多条数/天")
|
||||
rest_minutes = models.IntegerField(default=30, verbose_name="每轮休息分钟")
|
||||
collection_min = models.IntegerField(default=10, verbose_name="收藏最少个数/天")
|
||||
collection_max = models.IntegerField(default=50, verbose_name="收藏最多个数/天")
|
||||
message_interval = models.IntegerField(default=30, verbose_name="打招呼间隔秒")
|
||||
min_amount = models.IntegerField(null=True, blank=True, verbose_name="最小金额")
|
||||
max_amount = models.IntegerField(null=True, blank=True, verbose_name="最大金额")
|
||||
created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
|
||||
updated_at = models.DateTimeField(auto_now=True, verbose_name="更新时间")
|
||||
|
||||
class Meta:
|
||||
db_table = "filter_config"
|
||||
verbose_name = "筛选配置"
|
||||
verbose_name_plural = verbose_name
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class RecruitFilterSnapshot(models.Model):
|
||||
"""Per-account site filter snapshot fetched from zhipin recommend page."""
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ DRF 序列化器。
|
||||
from rest_framework import serializers
|
||||
|
||||
from server.models import (
|
||||
BossAccount, TaskLog, FilterConfig, RecruitFilterSnapshot, ChatScript, ContactRecord, SystemConfig,
|
||||
BossAccount, TaskLog, RecruitFilterSnapshot, ChatScript, ContactRecord, SystemConfig,
|
||||
FollowUpConfig, FollowUpScript, FollowUpRecord
|
||||
)
|
||||
|
||||
@@ -89,49 +89,6 @@ class LoginSerializer(serializers.Serializer):
|
||||
password = serializers.CharField(max_length=128)
|
||||
|
||||
|
||||
# ────────────────────────── 筛选配置 ──────────────────────────
|
||||
|
||||
# 请求中可能出现的“金额”字段别名,统一映射为 min_amount / max_amount,保证请求与响应字段名一致
|
||||
FILTER_AMOUNT_ALIASES = {
|
||||
"minAmount": "min_amount",
|
||||
"maxAmount": "max_amount",
|
||||
"最小金额": "min_amount",
|
||||
"最大金额": "max_amount",
|
||||
}
|
||||
|
||||
|
||||
class FilterConfigSerializer(serializers.ModelSerializer):
|
||||
"""筛选配置:列表/详情返回 name, position_keywords, city, salary_min, salary_max, experience, education, is_active 等。"""
|
||||
|
||||
class Meta:
|
||||
model = FilterConfig
|
||||
fields = "__all__"
|
||||
read_only_fields = ["id", "created_at", "updated_at"]
|
||||
|
||||
def to_internal_value(self, data):
|
||||
"""请求中兼容 minAmount/maxAmount 等别名;支持 multipart/form-data(QueryDict/多值列表)。"""
|
||||
if data is None:
|
||||
data = {}
|
||||
else:
|
||||
data = dict(data)
|
||||
# multipart 解析后字段值可能是 list(如 ["测试配置"]),需展平为标量
|
||||
for key in list(data.keys()):
|
||||
val = data[key]
|
||||
if isinstance(val, (list, tuple)):
|
||||
data[key] = val[0] if len(val) > 0 else ""
|
||||
for alias, canonical in FILTER_AMOUNT_ALIASES.items():
|
||||
if alias in data and canonical not in data:
|
||||
data[canonical] = data.pop(alias)
|
||||
# 请求里 is_active 可能是字符串 "true"/"false" 或单元素列表
|
||||
raw = data.get("is_active")
|
||||
if raw is not None:
|
||||
if isinstance(raw, (list, tuple)):
|
||||
raw = raw[0] if raw else ""
|
||||
if isinstance(raw, str):
|
||||
data["is_active"] = raw.lower() in ("true", "1", "yes", "是")
|
||||
return super().to_internal_value(data)
|
||||
|
||||
|
||||
# ────────────────────────── 话术 ──────────────────────────
|
||||
|
||||
class RecruitFilterSnapshotSerializer(serializers.ModelSerializer):
|
||||
|
||||
@@ -35,10 +35,8 @@ urlpatterns = [
|
||||
path("api/accounts/worker/<str:worker_id>", accounts.account_list_by_worker),
|
||||
path("api/accounts/<int:account_id>", accounts.account_detail),
|
||||
|
||||
# ─── 筛选配置 ───
|
||||
path("api/filters", filters.filter_list),
|
||||
# ─── 招聘筛选快照 ───
|
||||
path("api/filters/options", filters.recruit_filter_options),
|
||||
path("api/filters/<int:pk>", filters.filter_detail),
|
||||
|
||||
# ─── 话术管理 ───
|
||||
path("api/scripts", scripts.script_list),
|
||||
|
||||
@@ -38,6 +38,8 @@ def parse_args():
|
||||
parser.add_argument("--worker-id", default=config.WORKER_ID, help=f"Worker ID (default: {config.WORKER_ID})")
|
||||
parser.add_argument("--worker-name", default=config.WORKER_NAME, help=f"Worker name (default: {config.WORKER_NAME})")
|
||||
parser.add_argument("--bit-api", default=config.BIT_API_BASE, help=f"BitBrowser local API URL (default: {config.BIT_API_BASE})")
|
||||
parser.add_argument("--sync-browser-id", default="", help="Browser ID used for startup recruit filter sync")
|
||||
parser.add_argument("--sync-browser-name", default="", help="Browser name used for startup recruit filter sync")
|
||||
parser.add_argument("--no-tunnel", action="store_true", help="Disable tunnel client")
|
||||
return parser.parse_args()
|
||||
|
||||
@@ -71,6 +73,8 @@ async def run(args):
|
||||
bootstrap_result = bootstrap_recruit_filter_snapshot(
|
||||
worker_id=args.worker_id,
|
||||
bit_api_base=args.bit_api,
|
||||
account_name=args.sync_browser_name,
|
||||
browser_id=args.sync_browser_id,
|
||||
logger=logger,
|
||||
)
|
||||
if bootstrap_result:
|
||||
@@ -158,4 +162,3 @@ def main():
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
@@ -7,86 +7,28 @@ Recruit filter snapshot sync:
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import importlib.util
|
||||
import logging
|
||||
from functools import lru_cache
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from worker.bit_browser import BitBrowserAPI
|
||||
from worker.browser_control import connect_browser
|
||||
|
||||
SCRIPT_ONE_PATH = Path(__file__).resolve().parent.parent / "1.py"
|
||||
|
||||
|
||||
FILTER_API = "wapi/zpblock/recommend/filters"
|
||||
RECOMMEND_URL = "https://www.zhipin.com/web/chat/recommend"
|
||||
@lru_cache(maxsize=1)
|
||||
def _load_main1():
|
||||
spec = importlib.util.spec_from_file_location("boss_dp_script_one", SCRIPT_ONE_PATH)
|
||||
if spec is None or spec.loader is None:
|
||||
raise RuntimeError(f"无法加载脚本: {SCRIPT_ONE_PATH}")
|
||||
|
||||
|
||||
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)
|
||||
return parsed if isinstance(parsed, dict) else {}
|
||||
except Exception:
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
def _dedupe(items: List[str]) -> List[str]:
|
||||
seen = set()
|
||||
result: List[str] = []
|
||||
for item in items:
|
||||
value = str(item or "").strip()
|
||||
if not value or value in seen:
|
||||
continue
|
||||
seen.add(value)
|
||||
result.append(value)
|
||||
return result
|
||||
|
||||
|
||||
def _parse_groups(raw_filters: List[dict]) -> tuple[list[dict], list[str]]:
|
||||
groups: List[dict] = []
|
||||
flat_options: List[str] = []
|
||||
|
||||
for idx, item in enumerate(raw_filters or []):
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
name = str(item.get("name", "")).strip()
|
||||
if not name:
|
||||
continue
|
||||
|
||||
options: List[str] = []
|
||||
raw_options = item.get("options")
|
||||
if isinstance(raw_options, list):
|
||||
for opt in raw_options:
|
||||
if not isinstance(opt, dict):
|
||||
continue
|
||||
opt_name = str(opt.get("name", "")).strip()
|
||||
if opt_name:
|
||||
options.append(opt_name)
|
||||
options = _dedupe(options)
|
||||
|
||||
group: Dict[str, Any] = {
|
||||
"name": name,
|
||||
"order": idx,
|
||||
"options": options,
|
||||
}
|
||||
|
||||
start = item.get("start")
|
||||
end = item.get("end")
|
||||
try:
|
||||
if start is not None and end is not None:
|
||||
group["range"] = {"start": int(start), "end": int(end)}
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
groups.append(group)
|
||||
flat_options.extend(options)
|
||||
|
||||
return groups, _dedupe(flat_options)
|
||||
module = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(module)
|
||||
main1 = getattr(module, "main1", None)
|
||||
if not callable(main1):
|
||||
raise RuntimeError(f"{SCRIPT_ONE_PATH} 中未找到可调用的 main1")
|
||||
return main1
|
||||
|
||||
|
||||
def fetch_recruit_filters_from_site(
|
||||
@@ -99,30 +41,25 @@ def fetch_recruit_filters_from_site(
|
||||
) -> Dict[str, Any]:
|
||||
"""Fetch recruit filters from target site using one BitBrowser profile."""
|
||||
log = logger or logging.getLogger("worker.recruit_filter_sync")
|
||||
bit_api = BitBrowserAPI(bit_api_base)
|
||||
|
||||
main1 = _load_main1()
|
||||
resolved_browser_id = (browser_id or "").strip()
|
||||
if resolved_browser_id.startswith("name:"):
|
||||
resolved_browser_id = ""
|
||||
|
||||
_, port = bit_api.get_browser_for_drission(
|
||||
payload = main1(
|
||||
use_local_chrome=False,
|
||||
bit_api_base=bit_api_base,
|
||||
browser_name=(account_name or "").strip() or None,
|
||||
browser_id=resolved_browser_id or None,
|
||||
name=(account_name or "").strip() or None,
|
||||
timeout_sec=timeout_sec,
|
||||
echo=False,
|
||||
)
|
||||
browser = connect_browser(port=port)
|
||||
tab = browser.latest_tab
|
||||
if not isinstance(payload, dict):
|
||||
raise RuntimeError("main1 未返回有效筛选数据")
|
||||
|
||||
tab.listen.start(FILTER_API)
|
||||
tab.get(RECOMMEND_URL)
|
||||
packet = tab.listen.wait(timeout=timeout_sec)
|
||||
body = _packet_body(packet)
|
||||
zp_data = body.get("zpData", {}) if isinstance(body, dict) else {}
|
||||
vip_filter = zp_data.get("vipFilter", {}) if isinstance(zp_data, dict) else {}
|
||||
raw_filters = vip_filter.get("filters", []) if isinstance(vip_filter, dict) else []
|
||||
if not isinstance(raw_filters, list):
|
||||
raw_filters = []
|
||||
|
||||
groups, flat_options = _parse_groups(raw_filters)
|
||||
groups = payload.get("groups", [])
|
||||
flat_options = payload.get("flat_options", [])
|
||||
raw_payload = payload.get("raw_payload", {})
|
||||
log.info(
|
||||
"Fetched recruit filters: account=%s groups=%d options=%d",
|
||||
account_name,
|
||||
@@ -132,7 +69,7 @@ def fetch_recruit_filters_from_site(
|
||||
return {
|
||||
"groups": groups,
|
||||
"flat_options": flat_options,
|
||||
"raw_payload": {"filters": raw_filters},
|
||||
"raw_payload": raw_payload if isinstance(raw_payload, dict) else {},
|
||||
}
|
||||
|
||||
|
||||
@@ -189,6 +126,8 @@ def bootstrap_recruit_filter_snapshot(
|
||||
*,
|
||||
worker_id: str,
|
||||
bit_api_base: str,
|
||||
account_name: str = "",
|
||||
browser_id: str = "",
|
||||
logger: Optional[logging.Logger] = None,
|
||||
) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
@@ -198,6 +137,23 @@ def bootstrap_recruit_filter_snapshot(
|
||||
log = logger or logging.getLogger("worker.recruit_filter_sync")
|
||||
from server.models import BossAccount
|
||||
|
||||
selected_account_name = (account_name or "").strip()
|
||||
selected_browser_id = (browser_id or "").strip()
|
||||
if selected_account_name:
|
||||
payload = sync_recruit_filters_for_account(
|
||||
worker_id=worker_id,
|
||||
bit_api_base=bit_api_base,
|
||||
account_name=selected_account_name,
|
||||
browser_id=selected_browser_id,
|
||||
logger=log,
|
||||
)
|
||||
return {
|
||||
"worker_id": worker_id,
|
||||
"account_name": selected_account_name,
|
||||
"groups": len(payload.get("groups", [])),
|
||||
"flat_options": len(payload.get("flat_options", [])),
|
||||
}
|
||||
|
||||
accounts = (
|
||||
BossAccount.objects
|
||||
.filter(worker_id=worker_id)
|
||||
@@ -236,4 +192,3 @@ def bootstrap_recruit_filter_snapshot(
|
||||
if last_error:
|
||||
log.warning("Recruit filter bootstrap skipped: all accounts failed for worker=%s", worker_id)
|
||||
return None
|
||||
|
||||
|
||||
@@ -13,7 +13,6 @@ import json
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Any, Callable, Coroutine, Dict, List, Optional
|
||||
|
||||
from common.protocol import TaskType
|
||||
@@ -616,116 +615,10 @@ class BossReplyHandler(BaseTaskHandler):
|
||||
return found[:3]
|
||||
|
||||
def _apply_filters(self, friend_list: list) -> list:
|
||||
"""应用筛选条件过滤候选人列表。"""
|
||||
try:
|
||||
from server.models import FilterConfig
|
||||
|
||||
# 获取启用的筛选配置
|
||||
filter_config = FilterConfig.objects.filter(is_active=True).first()
|
||||
if not filter_config:
|
||||
self.logger.info("未找到启用的筛选配置,跳过筛选")
|
||||
"""旧筛选配置已下线,当前直接返回原始候选人列表。"""
|
||||
self.logger.info("旧版筛选配置已移除,reply 流程不再额外过滤候选人")
|
||||
return friend_list
|
||||
|
||||
filtered = []
|
||||
for friend in friend_list:
|
||||
# 筛选活跃度(最后上线时间)
|
||||
last_time = friend.get("lastTime", "")
|
||||
if not self._check_activity(last_time, filter_config.activity):
|
||||
continue
|
||||
|
||||
# 从简历信息中获取年龄、学历、期望职位
|
||||
resume = friend.get("resume", {}) or {}
|
||||
|
||||
# 筛选年龄
|
||||
age = resume.get("age")
|
||||
if age and not (filter_config.age_min <= int(age) <= filter_config.age_max):
|
||||
continue
|
||||
|
||||
# 筛选学历
|
||||
education = resume.get("education", "")
|
||||
if filter_config.education != "不限" and education:
|
||||
if not self._check_education(education, filter_config.education):
|
||||
continue
|
||||
|
||||
# 筛选期望职位
|
||||
job_name = friend.get("jobName", "")
|
||||
if filter_config.positions and job_name:
|
||||
if not any(pos in job_name for pos in filter_config.positions):
|
||||
continue
|
||||
|
||||
filtered.append(friend)
|
||||
|
||||
self.logger.info("筛选前: %d 人,筛选后: %d 人", len(friend_list), len(filtered))
|
||||
return filtered
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("应用筛选条件失败: %s,返回原列表", e)
|
||||
return friend_list
|
||||
|
||||
def _check_activity(self, last_time: str, activity_filter: str) -> bool:
|
||||
"""检查活跃度是否符合要求。"""
|
||||
if activity_filter == "不限":
|
||||
return True
|
||||
|
||||
try:
|
||||
# 解析时间字符串
|
||||
now = datetime.now()
|
||||
|
||||
if "昨天" in last_time:
|
||||
last_active = now - timedelta(days=1)
|
||||
elif "今天" in last_time or "刚刚" in last_time:
|
||||
last_active = now
|
||||
elif "月" in last_time and "日" in last_time:
|
||||
# 格式如 "03月03日"
|
||||
match = re.search(r"(\d+)月(\d+)日", last_time)
|
||||
if match:
|
||||
month = int(match.group(1))
|
||||
day = int(match.group(2))
|
||||
year = now.year
|
||||
# 如果月份大于当前月份,说明是去年的
|
||||
if month > now.month:
|
||||
year -= 1
|
||||
last_active = datetime(year, month, day)
|
||||
else:
|
||||
return True
|
||||
else:
|
||||
return True
|
||||
|
||||
# 计算天数差
|
||||
days_diff = (now - last_active).days
|
||||
|
||||
# 根据筛选条件判断
|
||||
if activity_filter == "今天活跃":
|
||||
return days_diff == 0
|
||||
elif activity_filter == "3天内活跃":
|
||||
return days_diff <= 3
|
||||
elif activity_filter == "本周活跃":
|
||||
return days_diff <= 7
|
||||
elif activity_filter == "本月活跃":
|
||||
return days_diff <= 30
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning("解析活跃度时间失败: %s, last_time=%s", e, last_time)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _check_education(candidate_edu: str, required_edu: str) -> bool:
|
||||
"""检查学历是否符合要求。"""
|
||||
edu_levels = ["初中", "高中", "中专", "大专", "本科", "硕士", "博士"]
|
||||
|
||||
try:
|
||||
candidate_level = next((i for i, edu in enumerate(edu_levels) if edu in candidate_edu), -1)
|
||||
required_level = next((i for i, edu in enumerate(edu_levels) if edu in required_edu), -1)
|
||||
|
||||
if candidate_level == -1 or required_level == -1:
|
||||
return True
|
||||
|
||||
return candidate_level >= required_level
|
||||
except Exception:
|
||||
return True
|
||||
|
||||
def _filter_my_messages(self, messages: list) -> list:
|
||||
"""过滤掉自己发送的消息,只保留对方的消息。"""
|
||||
filtered = []
|
||||
@@ -958,4 +851,3 @@ class BossReplyHandler(BaseTaskHandler):
|
||||
self.logger.error("等待回复失败: %s", e)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
Reference in New Issue
Block a user