feat(boss): 登录后自动关闭治理公告弹窗;沟通页新招呼/未读 tab 初始化

- 新增 GOVERNANCE_NOTICE_DIALOG_* 选择器与 dismissGovernanceNoticeDialog(),
  在 launchBrowserAndNavigateToChat 与 startBossAutoBrowse 登录后调用,避免阻塞自动化。
- 沟通页:先切「新招呼」再强制点「未读」刷新列表;switchToTab 支持 force 选项。
- 文档:recruiter_architecture §14 弹窗清单与 §6 常量更新;CLAUDE.md 补充登录后弹窗说明;
  新增 plan/README.md、plan/chat_page_tab_navigation.md。

Made-with: Cursor
This commit is contained in:
rqi14
2026-03-27 12:04:13 +08:00
parent 3fb7089c9e
commit 0bb94409a2
7 changed files with 524 additions and 29 deletions

View File

@@ -101,6 +101,11 @@ Key files and their roles:
Anti-detection: stealth + laodeng + anonymize-ua plugins; all clicks via `ghost-cursor` (`createHumanCursor`); random delays via `sleepWithRandomDelay`.
**Known post-login popups** — all must be auto-dismissed or automation will hang:
- **Governance notice** (`dialog-uninstall-extension`) — appears every login; handled by `dismissGovernanceNoticeDialog(page)` in `index.mjs`, called after login in both `launchBrowserAndNavigateToChat` and `startBossAutoBrowse`. Confirm button is `div.confirm-btn` (a `<div>` styled with a background image, not a `<button>`). See `plan/recruiter_architecture.md §14.1` and `examples/BOSS直聘-治理公告*.html`.
- **Intent dialog** (`.op-btn.rightbar-item div.dialog-container`) — per-session, per-conversation; handled in `chat-page-processor.mjs`.
- When selectors break, update `constant.mjs` first, then follow the checklist in `plan/recruiter_architecture.md §14.5`.
## Code Style
Enforced by eslint + prettier in `packages/ui`:

View File

@@ -27,6 +27,7 @@ import {
CHAT_PAGE_ITEM_UNREAD_SELECTOR,
CHAT_PAGE_ALL_FILTER_SELECTOR,
CHAT_PAGE_UNREAD_FILTER_SELECTOR,
CHAT_PAGE_TAB_NEW_GREET_SELECTOR,
CHAT_PAGE_NAME_SELECTOR,
CHAT_PAGE_JOB_SELECTOR,
CHAT_PAGE_PREVIEW_RESUME_BTN_SELECTOR,
@@ -391,14 +392,16 @@ export default async function startBossChatPageProcess (hooksFromCaller, options
// ────────────────────────────────────────────────────────────────────────────
// 内部辅助:切换到指定 tab封闭 page、cursor
// ────────────────────────────────────────────────────────────────────────────
const switchToTab = async (selector, tabName) => {
const isActive = await page.evaluate(
(sel) => document.querySelector(sel)?.classList.contains('active') ?? false,
selector
)
if (isActive) {
logDebug(`${LOG} 已在「${tabName}」tab`)
return
const switchToTab = async (selector, tabName, opts = {}) => {
if (!opts.force) {
const isActive = await page.evaluate(
(sel) => document.querySelector(sel)?.classList.contains('active') ?? false,
selector
)
if (isActive) {
logDebug(`${LOG} 已在「${tabName}」tab`)
return
}
}
logInfo(`${LOG} 切换到「${tabName}」tab...`)
const tabEl = await page.$(selector)
@@ -762,6 +765,16 @@ export default async function startBossChatPageProcess (hooksFromCaller, options
}
// ────────────────────────────────────────────────────────────────────────────
// ── 职位 tab 初始化:切换到「新招呼」分类,再强制点击「未读」触发列表刷新 ────────
// 必须先进入「新招呼」分类,才能只扫描当前职位下候选人主动发来的招呼,避免遍历其他类型会话。
// 「未读」tab 只有被实际点击时 BOSS 才会刷新列表若上次运行后页面已停在「未读」tab
// 不点击则不会刷新已处理过的会话仍会出现导致重复操作。因此此处强制点击force: true
await switchToTab(CHAT_PAGE_TAB_NEW_GREET_SELECTOR, '新招呼', { force: true })
await sleepWithRandomDelay(300, 500)
await switchToTab(CHAT_PAGE_UNREAD_FILTER_SELECTOR, '未读', { force: true })
await sleepWithRandomDelay(400, 600)
// ────────────────────────────────────────────────────────────────────────────
// ── 验证恢复:若上次被验证中断,优先重试被中断的候选人 ────────────────────────
if (retryCandidate) {
logInfo(`${LOG} ── 验证恢复:重试被中断候选人 ${retryCandidate.geekName}${retryCandidate.encryptGeekId})──`)
@@ -781,9 +794,9 @@ export default async function startBossChatPageProcess (hooksFromCaller, options
await sleepWithRandomDelay(300)
}
// ── 正常扫描:切换到「未读」tab处理未读会话 ───────────────────────────────
await switchToTab(CHAT_PAGE_UNREAD_FILTER_SELECTOR, '未读')
await sleepWithRandomDelay(300)
// ── 正常扫描:处理「新招呼」分类下的未读会话 ─────────────────────────────────
// 「新招呼」分类与「未读」tab 已在上方初始化阶段完成切换force: true此处直接解析列表。
// 若经过 retryCandidate 流程retry 结束时已切回「未读」tab状态同样正确。
const conversations = await parseConversationList(page)
logDebug(`${LOG} DOM 解析到 ${conversations.length} 条会话`)

View File

@@ -217,3 +217,26 @@ export const CHAT_PAGE_INTENT_DIALOG_CLOSE_SELECTOR = '.op-btn.rightbar-item div
* HTML: div.chat-label-item[title="全部"],选中态有 class selected。
*/
export const CHAT_PAGE_TAB_ALL_SELECTOR = '.chat-label-item[title="全部"]'
// =============================================================================
// 三、治理公告弹窗(登录后出现,须点击「我已知晓」才能继续操作)
// =============================================================================
/**
* 治理公告弹窗dialog-uninstall-extension容器选择器。
* BOSS 每次登录后会弹出此公告,告知平台禁止使用第三方自动化工具。
* HTML: div.boss-popup__wrapper.boss-dialog.boss-dialog__wrapper.dialog-uninstall-extension
*/
export const GOVERNANCE_NOTICE_DIALOG_SELECTOR = '.dialog-uninstall-extension'
/**
* 治理公告弹窗内的「我已知晓」确认按钮div.confirm-btn背景图模拟按钮样式
* HTML: div.dialog-uninstall-extension div.uninstall-extension div.content div.confirm-btn
*/
export const GOVERNANCE_NOTICE_DIALOG_CONFIRM_BTN_SELECTOR = '.dialog-uninstall-extension .confirm-btn'
/**
* 沟通页:左侧会话列表分类 tab——「新招呼」候选人主动发来招呼的会话
* 每次开始处理前须先点击此 tab确保只扫描新招呼消息避免遍历其他类型会话。
* HTML: div.chat-label-item[title="新招呼"],选中态有 class selected。
*/
export const CHAT_PAGE_TAB_NEW_GREET_SELECTOR = '.chat-label-item[title="新招呼"]'

View File

@@ -9,7 +9,9 @@ import {
BOSS_RECOMMEND_PAGE_URL,
BOSS_CHAT_PAGE_URL,
RECOMMEND_JOB_DROPDOWN_LABEL_SELECTOR,
RECOMMEND_JOB_ITEM_SELECTOR
RECOMMEND_JOB_ITEM_SELECTOR,
GOVERNANCE_NOTICE_DIALOG_SELECTOR,
GOVERNANCE_NOTICE_DIALOG_CONFIRM_BTN_SELECTOR
} from './constant.mjs'
import { setupNetworkInterceptor, setupCanvasTextHook } from './resume-extractor.mjs'
import { parseCandidateList, filterCandidates, scrollAndLoadMore } from './candidate-processor.mjs'
@@ -60,6 +62,34 @@ export async function initPuppeteer () {
}
}
/**
* 关闭登录后弹出的「治理公告」弹窗(点击「我已知晓」确认按钮)。
* 该弹窗在每次登录后必现,不处理会导致后续自动化操作卡死超时。
* @param {import('puppeteer').Page} page
*/
async function dismissGovernanceNoticeDialog (page) {
try {
const confirmBtn = await page.$(GOVERNANCE_NOTICE_DIALOG_CONFIRM_BTN_SELECTOR)
if (!confirmBtn) return
logInfo('[boss-auto-browse] 检测到「治理公告」弹窗,点击「我已知晓」关闭...')
try {
const box = await confirmBtn.boundingBox().catch(() => null)
if (box) {
await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2)
} else {
await confirmBtn.click()
}
} catch {
await confirmBtn.click().catch(() => {})
}
await page.waitForSelector(GOVERNANCE_NOTICE_DIALOG_SELECTOR, { hidden: true, timeout: 5000 }).catch(() => {})
logInfo('[boss-auto-browse] 「治理公告」弹窗已关闭')
await sleep(300)
} catch {
// 弹窗不存在或关闭失败时静默继续
}
}
/** 招聘端 localStorage 生效的页面 URL与 geek 端一致使用 desktop */
const localStoragePageUrl = 'https://www.zhipin.com/desktop/'
@@ -86,6 +116,7 @@ export async function launchBrowserAndNavigateToChat () {
await page.goto(BOSS_CHAT_PAGE_URL, { timeout: 60 * 1000 })
await page.waitForFunction(() => document.readyState === 'complete', { timeout: 120 * 1000 })
await new Promise(r => setTimeout(r, 1500))
await dismissGovernanceNoticeDialog(page)
return { browser, page }
}
@@ -304,6 +335,9 @@ export default async function startBossAutoBrowse (hooksFromCaller, opts = {}) {
await storeStorage(page).catch(() => {})
}
// 关闭登录后弹出的「治理公告」弹窗(每次登录必现,不处理会阻塞后续操作)
await dismissGovernanceNoticeDialog(page)
// 切换职位(若指定了 jobId 且非全部职位标志)
if (jobId && jobId !== '-1' && jobId !== '0') {
await switchRecommendJobId(page, jobId)

86
plan/README.md Normal file
View File

@@ -0,0 +1,86 @@
# plan/ 文档索引
本目录存放招聘端BOSS相关的设计、流程说明与阶段性记录。单篇篇幅较长时**先读本索引再点进对应文件**。
---
## 从这里开始
| 文档 | 说明 |
|------|------|
| [recruiter_architecture.md](recruiter_architecture.md) | **招聘端架构总览**UI / IPC / Worker、`boss-auto-browse-and-chat` 模块分工、主循环要点。协作开发时优先读这篇。 |
| [recruiter_chat_page_hr_guide.md](recruiter_chat_page_hr_guide.md) | **给 HR 的操作说明**登录、职位筛选、LLM、沟通页启动非开发者入口。 |
---
## 架构与页面结构
| 文档 | 说明 |
|------|------|
| [boss_auto_browse_tabs.md](boss_auto_browse_tabs.md) | 自动化里「沟通 Tab + 推荐牛人 Tab」双 Tab 设计、URL、主流程顺序。 |
| [chat_page_tab_navigation.md](chat_page_tab_navigation.md) | 沟通页左侧「会话类型 Tab」与「已读/未读」两套控件、导航顺序与实现注意点。 |
| [recommend_page_flow.md](recommend_page_flow.md) | 推荐牛人页完整逻辑iframe 结构、选择器、各 `.mjs` 分工。 |
---
## 沟通页 · 简历与筛选
| 文档 | 说明 |
|------|------|
| [chat_page_resume_flow.md](chat_page_resume_flow.md) | 在线简历 → 关键词/LLM → 附件简历 → PDF 的流程与数据链路说明。 |
| [cv_canvas_solution.md](cv_canvas_solution.md) | 在线简历 Canvas/WASM 加密与提取思路fillText Hook、`get_export_geek_detail_info` 等)。 |
| [recruiter_llm_integration.md](recruiter_llm_integration.md) | 招聘端 LLM`boss-llm.json`、多用途模型、Rubric、与实现状态。 |
| [recruiter_debug_tool.md](recruiter_debug_tool.md) | 招聘端调试工具进程架构、IPC、与正式流程一致的操作栈。 |
---
## 集成与扩展规划
| 文档 | 说明 |
|------|------|
| [webhook_integration.md](webhook_integration.md) | Webhook配置、Payload、相关源码路径、与任务结束时的发送时机。 |
| [multi-job-switching.md](multi-job_switching.md) | 多职位同步、`boss-jobs-config.json`、按职位配置与顺序执行的设计草案。 |
---
## 工程细节
| 文档 | 说明 |
|------|------|
| [logger_usage.md](logger_usage.md) | `logger.mjs` 级别、API、`boss-recruiter.json``logLevel` 的用法。 |
| [recruiter_mouse_trajectory.md](recruiter_mouse_trajectory.md) | 拟人鼠标ghost-cursor 等)要求与适用范围。 |
---
## 状态与里程碑(可能部分重叠)
| 文档 | 说明 |
|------|------|
| [current_status_2026_03_18.md](current_status_2026_03_18.md) | 截至 2026-03-18已实现能力、已知问题尤其推荐页状态/去重)、下一步计划。 |
| [STATUS_2026-03-18.md](STATUS_2026-03-18.md) | 同日另一份阶段性现状(沟通可用、推荐页状态判定待加强等),可与上一篇对照阅读。 |
---
## 历史方案与并行开发稿(篇幅大、偏「当时怎么拆任务」)
以下文件多为早期规划或已 resolve 的长文,**需要考古或对齐旧 Prompt 时再打开**。
| 文档 | 说明 |
|------|------|
| [implementation_plan.md.resolved](implementation_plan.md.resolved) | 招聘端功能扩展总体方案可行性、Phase、工时等。 |
| [parallel_execution_plan.md.resolved](parallel_execution_plan.md.resolved) | 分 Phase 并行执行的 Agent Prompt 集合;文中引用 `recruiter_mouse_trajectory.md`。 |
> 说明:`.gitignore` 中忽略了 `*.resolved` 等类型;若仓库中未跟踪这些文件,以你本地是否存在为准。
---
## 未列入索引的文件
| 类型 | 说明 |
|------|------|
| `log.txt` / `log_recommend.txt` | 运行日志样例,非设计文档。 |
| `.gitignore` | 忽略规则。 |
---
*索引维护:新增或拆分大文档时,请在本 README 中补一行条目,避免目录再次难以浏览。*

View File

@@ -0,0 +1,222 @@
# 沟通页 Tab 导航行为与「新招呼 + 未读」初始化设计
## 背景
本文档记录 **招聘端沟通页**`/web/chat/index`)左侧会话列表的 Tab 结构、
正确的导航顺序、已知的 BOSS 前端刷新特性,以及对应的自动化实现设计。
相关代码:
- `packages/boss-auto-browse-and-chat/chat-page-processor.mjs``startBossChatPageProcess`
- `packages/boss-auto-browse-and-chat/constant.mjs` — 所有选择器
---
## 1. 沟通页左侧面板的 UI 层次
沟通页左侧面板存在**两套独立的过滤控件**,功能和 DOM 结构完全不同,容易混淆:
### 1-A. 会话类型 Tab`.chat-label-item`
位于左侧列表的顶部,按**消息来源类型**分类:
| Tab 名称 | DOM 选择器 | 选中态 class | 含义 |
|---------|-----------|------------|------|
| 全部 | `.chat-label-item[title="全部"]` | `selected` | 不限类型 |
| **新招呼** | `.chat-label-item[title="新招呼"]` | `selected` | 候选人主动发来的第一条招呼 |
| 沟通中 | `.chat-label-item[title="沟通中"]` | `selected` | 已有来回消息的会话 |
| 已获取简历 | `.chat-label-item[title="已获取简历"]` | `selected` | 简历已获取 |
| 已交换微信 | `.chat-label-item[title="已交换微信"]` | `selected` | 微信已交换 |
> **注意**:选中态 class 是 `selected`,不是 `active`。`switchToTab` 的默认 active
> 检测用的是 `active`,因此对 `.chat-label-item` tab 不要依赖 active 检测,
> 应使用 `force: true` 强制点击。
### 1-B. 已读/未读状态 Tab`.chat-message-filter-left span`
位于 1-A 之下,按**已读/未读状态**过滤当前类型内的会话:
| Tab 名称 | DOM 选择器 | 选中态 class | 含义 |
|---------|-----------|------------|------|
| 全部 | `.chat-message-filter-left span:nth-child(1)` | `active` | 不限已读/未读 |
| **未读** | `.chat-message-filter-left span:nth-child(2)` | `active` | 只显示未读会话 |
> 这里的"全部"与 1-A 的"全部"是**不同的控件**选择器、class 名、语义均不同,
> 不要混淆。
---
## 2. 正确的手动操作顺序(同事记录的复现路径)
```
1. 点击「全部职位」(展开顶部职位下拉框)
2. 点击目标职位(如「实验室技术员」)→ 会话列表切换为该职位
3. 点击「新招呼」1-A tab→ 只显示候选人主动打招呼的会话
4. 点击「未读」1-B tab→ BOSS 刷新未读列表
5. 开始逐条处理
```
步骤 3新招呼和步骤 4未读缺一不可
- **省略步骤 3** → 在「全部类型」下,工具会看到「沟通中」「已获取简历」等其他类型的候选人,
处理范围远超预期(遍历全部职位的全部类型消息)。
- **省略步骤 4 或不强制点击** → BOSS 不刷新列表,上次已处理(已读)的候选人会继续出现,
导致重复操作(详见第 3 节)。
---
## 3. BOSS 「未读」列表不自动刷新的特性
BOSS 直聘沟通页的**未读会话列表不会自动轮询刷新**。
以下两种操作能使其刷新:
1. **整页 reload**`page.reload()` 或 F5
2. **手动点击「未读」tab**
如果程序已在「未读」tab 停留,且直接解析 DOM解析到的是**上次点击时的快照**
而非当前真实未读状态。已处理(点击后已读)的会话不会从列表消失。
### 旧代码的 bug
`switchToTab` 的实现含有如下提前返回逻辑:
```js
if (isActive) {
logDebug(`已在「${tabName}」tab`)
return // ← 跳过了点击BOSS 不会刷新列表
}
```
如果上次运行结束时页面停留在「未读」tab下次运行到达这段代码时
`isActive === true`,点击被跳过 → 列表未刷新 → 已处理的候选人被重复遍历。
### 修复方案
在每次 `startBossChatPageProcess` 进入处理循环前,用 `force: true` 强制点击
「新招呼」和「未读」,绕过 active 检测:
```js
await switchToTab(CHAT_PAGE_TAB_NEW_GREET_SELECTOR, '新招呼', { force: true })
await sleepWithRandomDelay(300, 500)
await switchToTab(CHAT_PAGE_UNREAD_FILTER_SELECTOR, '未读', { force: true })
await sleepWithRandomDelay(400, 600)
```
`switchToTab` 签名改为:
```js
const switchToTab = async (selector, tabName, opts = {}) => {
if (!opts.force) {
// 检测 active class已激活则跳过用于非刷新场景
}
// ... 拟人点击 ...
}
```
---
## 4. 职位下拉框(`.chat-top-job`)的切换逻辑
沟通页顶部有职位筛选下拉框,切换后左侧列表只显示该职位的会话。
| DOM 元素 | 常量名 | 说明 |
|---------|-------|------|
| 触发按钮 | `CHAT_PAGE_JOB_DROPDOWN_SELECTOR` | `.chat-top-job .ui-dropmenu-label` |
| 展开后列表项 | `CHAT_PAGE_JOB_ITEM_SELECTOR` | `.chat-top-job .ui-dropmenu-list li` |
切换函数:`switchChatPageJobId(page, jobId)`(同文件内部函数)。
- `jobId === '-1'``jobId == null` → 不切换(使用当前选中的全部职位)
- 切换后等待 400700 ms 让列表刷新
- 若下拉列表中未找到目标 jobId会打印 warning 并跳过(不抛异常)
> **调试提示**:如果切换职位后列表仍显示所有职位的消息,
> 先确认 `boss-jobs-config.json` 中的 `jobId` 字段值与 BOSS 页面
> 下拉框 `li[value]` 的值一致(通过 sync-boss-job-list IPC 同步可以保证这一点)。
---
## 5. 完整的 Tab 初始化序列(当前实现)
每次调用 `startBossChatPageProcess` 时,按以下顺序执行:
```
1. 确认当前在沟通页 URL否则 goto
2. setupNetworkInterceptor
3. waitForSelector(CHAT_PAGE_ITEM_SELECTOR, timeout=15s)
4. switchChatPageJobId若 jobId 有效)
5. 【新招呼 force】switchToTab(CHAT_PAGE_TAB_NEW_GREET_SELECTOR, { force: true })
6. 【未读 force】switchToTab(CHAT_PAGE_UNREAD_FILTER_SELECTOR, { force: true })
7. [可选] retryCandidate
switchToTab(ALL_FILTER, '全部') ← 找已读候选人
processOneCandidateConversation(...)
switchToTab(UNREAD_FILTER, '未读') ← 切回(不 force正常切换即可
8. parseConversationList → process loop
```
步骤 56 的「强制点击」保证无论上次运行的终止状态如何,都能进入正确的筛选视图,
且触发 BOSS 的未读列表数据刷新。
---
## 6. 验证 Tab 初始化是否生效的方法
### 日志关键字
成功路径(`logLevel: 'info'` 或更详细)应出现:
```
[chat-page-processor] 切换到「新招呼」tab...
[chat-page-processor] 「新招呼」tab 切换后列表已刷新
[chat-page-processor] 切换到「未读」tab...
[chat-page-processor] 「未读」tab 切换后列表已刷新
```
失败路径(元素未找到):
```
[chat-page-processor] 未找到「新招呼」tab 元素selector: .chat-label-item[title="新招呼"]
```
### 如果「新招呼」tab 找不到
可能原因:
1. **BOSS 更新了 DOM**:登录后手动打开沟通页,检查是否存在 `.chat-label-item[title="新招呼"]`
2. **账号下没有「新招呼」分类**:部分账号/状态下该 tab 不显示(如没有招聘职位),
此时 `switchToTab` 会打印 warning 并继续,不影响后续流程(降级为当前 tab
3. **中文 title 属性编码差异**:用浏览器控制台 `document.querySelector('.chat-label-item[title="新招呼"]')` 确认
### 如果处理后候选人仍然重复出现
检查步骤:
1. 确认日志中「未读」tab 的点击确实发生(不是 skip 返回)
2. 确认 `CHAT_PAGE_UNREAD_FILTER_SELECTOR` 指向的是 `span:nth-child(2)` 而不是第 1 个
3. 候选人可能已在数据库 `encryptGeekId` 记录中但未标记为 `contacted`
此时会被 `checkIfAlreadyContacted` 放过 → 检查数据库记录
---
## 7. retryCandidate 流程与 Tab 状态
`retryCandidate` 是验证中断恢复流程(被 BOSS 安全验证打断时),此阶段 Tab 状态:
| 步骤 | 1-A 类型 tab | 1-B 状态 tab | 说明 |
|------|------------|------------|------|
| 进入 retry 前 | 新招呼force 切入) | 未读force 切入) | 初始化阶段已设置 |
| retry 内切换 | 新招呼(保持) | **全部** | 候选人已读,需看全部 |
| retry 结束 | 新招呼(保持) | **未读** | 切回,准备正常扫描 |
| 正常扫描 | 新招呼 | 未读 | 初始化状态,直接 parseConversationList |
retry 结束时的 `switchToTab(UNREAD, '未读')` 不需要 `force: true`
因为这只是从「全部」切回「未读」的正常操作BOSS 会正常刷新列表。
---
## 8. 相关文件
| 文件 | 作用 |
|------|------|
| `packages/boss-auto-browse-and-chat/chat-page-processor.mjs` | `startBossChatPageProcess``switchChatPageJobId``switchToTab` |
| `packages/boss-auto-browse-and-chat/constant.mjs` | 所有 tab/选择器常量(`CHAT_PAGE_TAB_NEW_GREET_SELECTOR` 等) |
| `packages/ui/src/main/flow/BOSS_CHAT_PAGE_MAIN/index.ts` | Worker 入口,读取 `boss-jobs-config.json` 并按职位循环调用 `startBossChatPageProcess` |
| `plan/multi-job-switching.md` | 多职位配置文件结构、`sync-boss-job-list` IPC 实现 |
| `plan/boss_auto_browse_tabs.md` | 推荐牛人页与沟通页双 Tab 架构总览 |

View File

@@ -1,7 +1,7 @@
# 招聘端Recruiter/BOSS架构总览
> **定位**:供 AI Agent 快速理解招聘端全貌,用于协作开发时减少 token 消耗。
> 最后更新2026-03-16
> 最后更新2026-03-26
---
@@ -205,6 +205,8 @@ startBossChatPageProcess(hooks, { browser?, page? })
## 6. 关键常量constant.mjs
> 以下为主要常量摘录完整列表以源文件为准。BOSS 站点改版时常量可能失效,参见 §14.5 排查流程。
```js
// URL
BOSS_RECOMMEND_PAGE_URL = 'https://www.zhipin.com/web/chat/recommend'
@@ -212,24 +214,29 @@ BOSS_CHAT_INDEX_URL = 'https://www.zhipin.com/web/chat/index'
BOSS_CHAT_PAGE_URL = 'https://www.zhipin.com/web/chat/index'
// 推荐牛人页选择器
CANDIDATE_LIST_SELECTOR = '#recommend-list'
CANDIDATE_ITEM_SELECTOR = '#recommend-list ul.card-list > li.card-item'
CANDIDATE_NAME_SELECTOR = 'span.name'
CHAT_START_BUTTON_SELECTOR = 'div.operate-side button.btn-greet'
CANDIDATE_LIST_SELECTOR = 'ul.card-list'
CANDIDATE_ITEM_SELECTOR = 'ul.card-list > li.card-item'
CANDIDATE_NAME_SELECTOR = 'span.name'
CHAT_START_BUTTON_SELECTOR = 'button.btn-greet'
GREETING_SENT_KNOW_BTN_SELECTOR = 'div.dialog-wrap button.btn-sure-v2'
CONTINUE_CHAT_BUTTON_SELECTOR = 'div.operate-side > div > span > div > div'
CHAT_INPUT_SELECTOR = '#boss-chat-global-input'
CONTINUE_CHAT_BUTTON_SELECTOR = 'div.operate-side div.button-chat'
CHAT_INPUT_SELECTOR = '#boss-chat-global-input'
// 沟通页选择器CHAT_PAGE_* 前缀)
CHAT_PAGE_ITEM_SELECTOR = 'div.user-container > div > div:nth-child(2) > div:nth-child(1) > div'
CHAT_PAGE_NAME_SELECTOR = 'span.geek-name'
CHAT_PAGE_JOB_SELECTOR = 'span.source-job'
CHAT_PAGE_ONLINE_RESUME_SELECTOR = 'div.resume-btn-content > a > svg'
CHAT_PAGE_ATTACH_RESUME_BTN_SELECTOR = 'div.resume-btn-content > div > div.btn.resume-btn-file'
CHAT_PAGE_ASK_RESUME_CONFIRM_BTN_SELECTOR = 'div.ask-for-resume-confirm .content button.boss-btn-primary'
CHAT_PAGE_MESSAGE_ITEM_SELECTOR = 'div.chat-message-list.is-to-top > div > div:nth-child(2) > div'
CHAT_PAGE_PREVIEW_RESUME_BTN_SELECTOR = 'div.message-card-buttons > span'
CHAT_PAGE_DOWNLOAD_PDF_BTN_SELECTOR = 'div.resume-common-dialog div.resume-footer-wrap > div > div > div:nth-child(3)'
CHAT_PAGE_ITEM_SELECTOR = '.user-container .geek-item'
CHAT_PAGE_NAME_SELECTOR = 'span.geek-name'
CHAT_PAGE_JOB_SELECTOR = 'span.source-job'
CHAT_PAGE_ONLINE_RESUME_SELECTOR = 'a.resume-btn-online'
CHAT_PAGE_ATTACH_RESUME_BTN_SELECTOR = 'div.resume-btn-content .resume-btn-file'
CHAT_PAGE_ASK_RESUME_CONFIRM_BTN_SELECTOR = 'div.ask-for-resume-confirm > div.content > button.boss-btn-primary'
CHAT_PAGE_MESSAGE_ITEM_SELECTOR = '.chat-message-list .message-item'
CHAT_PAGE_PREVIEW_RESUME_BTN_SELECTOR = 'div.message-card-buttons > span.card-btn:only-child'
CHAT_PAGE_DOWNLOAD_PDF_BTN_SELECTOR = '.resume-common-dialog .attachment-resume-btns > .popover:nth-child(3)'
CHAT_PAGE_INTENT_DIALOG_KNOW_BTN_SELECTOR = '.op-btn.rightbar-item div.dialog-container div.button span'
// 治理公告弹窗登录后必现§14.1 详述)
GOVERNANCE_NOTICE_DIALOG_SELECTOR = '.dialog-uninstall-extension'
GOVERNANCE_NOTICE_DIALOG_CONFIRM_BTN_SELECTOR = '.dialog-uninstall-extension .confirm-btn'
```
---
@@ -414,7 +421,112 @@ const hooks = {
---
## 14. 相关文档
## 14. 已知弹窗及自动处理清单
BOSS直聘在各页面会弹出各类提示/公告弹窗,均需自动点击关闭,否则会遮挡操作区域或导致自动化卡死。以下列出所有已纳入代码处理的弹窗。
---
### 14.1 治理公告弹窗dialog-uninstall-extension
**何时出现:** 每次登录后包含首次加载、cookie 失效重新登录),浏览器导航到 BOSS 站点后必现。BOSS 借此告知平台禁止使用第三方自动化工具。
**外观:** 全屏遮罩,正中宽 580px 卡片,含平台公告文字;底部有一枚背景图模拟的「我已知晓」按钮(`div.confirm-btn`,非 `<button>`,以图片代替文字)。
**HTML 骨架(来自 `examples/BOSS直聘-治理公告 (2026_3_26 154151).html`**
```html
<!-- 挂载在 #boss-dynamic-dialog-<id>id 随机 -->
<div class="boss-popup__wrapper boss-dialog boss-dialog__wrapper dialog-uninstall-extension"
style="animation-duration:0s; width:580px; z-index:2002">
<div class="boss-popup__content">
<div class="boss-dialog__body">
<div data-v-4a24c2ed class="uninstall-extension">
<div data-v-4a24c2ed class="top"></div> <!-- 顶部装饰图 -->
<div data-v-4a24c2ed class="content">
<div data-v-4a24c2ed class="notice">...公告标题...</div>
<div data-v-4a24c2ed class="tips mb-24">...禁止使用第三方工具说明...</div>
<div data-v-4a24c2ed class="confirm-btn"></div> <!-- 「我已知晓」按钮(背景图) -->
</div>
</div>
</div>
</div>
<div ka class="boss-popup__close"><i class="icon-close"></i></div>
</div>
```
**关键选择器(在 `constant.mjs` 中定义):**
| 常量 | 选择器 | 用途 |
|------|--------|------|
| `GOVERNANCE_NOTICE_DIALOG_SELECTOR` | `.dialog-uninstall-extension` | 检测弹窗是否存在 |
| `GOVERNANCE_NOTICE_DIALOG_CONFIRM_BTN_SELECTOR` | `.dialog-uninstall-extension .confirm-btn` | 点击「我已知晓」 |
> **注意:** `confirm-btn` 是 `<div>` 而非 `<button>`,文字由背景图渲染,`page.$eval(selector, el => el.textContent)` 返回空字符串。
**处理函数:** `dismissGovernanceNoticeDialog(page)` — 在 `boss-auto-browse-and-chat/index.mjs` 中定义。
**调用位置:**
- `launchBrowserAndNavigateToChat()` — 导航到沟通页并等待 `readyState=complete` 之后
- `startBossAutoBrowse()` — 登录检查/等待登录块结束之后、切换职位之前
**Debug 提示:**
- 若弹窗出现但 `confirm-btn` 不可点击(`boundingBox()` 返回 null说明容器被 `overflow:hidden` 裁剪或弹窗尚未完成动画,应先等待 500ms 再重试。
- 弹窗 `z-index:2002`,会遮挡推荐牛人 iframe若候选人列表未能渲染优先排查此弹窗是否已关闭。
- 若选择器失效BOSS 改了 class打开 `examples/` 文件夹中最新保存的治理公告 HTML 快照,搜索 `confirm-btn` 或 `dialog-uninstall-extension` 重新确认。
---
### 14.2 意向沟通提示弹窗dialog-container
**何时出现:** 沟通页每次新浏览器会话切到某个会话时BOSS 视为新用户会弹出此提示(遮挡右侧附件简历等操作按钮)。
**关键选择器:**
| 常量 | 选择器 |
|------|--------|
| `CHAT_PAGE_INTENT_DIALOG_KNOW_BTN_SELECTOR` | `.op-btn.rightbar-item div.dialog-container div.button span` |
| `CHAT_PAGE_INTENT_DIALOG_CLOSE_SELECTOR` | `.op-btn.rightbar-item div.dialog-container div.iboss-close.close` |
**处理位置:** `chat-page-processor.mjs`,每次切换会话后(点击左侧会话项、等待右侧面板更新之后)立即检测并关闭。
---
### 14.3 已向牛人发送招呼弹窗
**何时出现:** 推荐牛人页,点击「打招呼」后弹出确认。
**关键选择器:** `GREETING_SENT_KNOW_BTN_SELECTOR = 'div.dialog-wrap button.btn-sure-v2'`
**处理位置:** `chat-handler.mjs` — `processCandidate()` 内打招呼流程。
---
### 14.4 「不感兴趣」原因弹窗
**何时出现:** 推荐牛人页 iframe 内,点击「不感兴趣」后弹出原因选择框。
**关键选择器:**
| 常量 | 选择器 |
|------|--------|
| `NOT_INTERESTED_REASON_POPUP_SELECTOR` | `div.card-reason-f1.show` |
| `NOT_INTERESTED_REASON_POPUP_CLOSE_SELECTOR` | `div.card-reason-f1.show div.close-icon` |
**处理位置:** `chat-handler.mjs` — `clickNotInterested()` 内。
---
### 14.5 维护历史 & 选择器失效排查流程
1. 在 Chrome DevTools 中打开对应 BOSS 页面,手动触发弹窗。
2. 右键元素 → Copy → Copy selector与 `constant.mjs` 中对应常量对比。
3. 更新 `constant.mjs` 中的常量值。
4. 在 `examples/` 目录下保存最新 HTML 快照(文件名含日期),以备后续对比。
5. 若弹窗结构变化较大同步更新本文档对应小节的「HTML 骨架」。
---
## 15. 相关文档
- [boss_auto_browse_tabs.md](boss_auto_browse_tabs.md) — 双 Tab 设计(沟通 vs 推荐牛人)
- [chat_page_resume_flow.md](chat_page_resume_flow.md) — 沟通页简历流程详述