diff --git a/CLAUDE.md b/CLAUDE.md
index 1c3b503..ff7669c 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -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 `
` styled with a background image, not a `
`). 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`:
diff --git a/packages/boss-auto-browse-and-chat/chat-page-processor.mjs b/packages/boss-auto-browse-and-chat/chat-page-processor.mjs
index 35f77db..c9ba551 100644
--- a/packages/boss-auto-browse-and-chat/chat-page-processor.mjs
+++ b/packages/boss-auto-browse-and-chat/chat-page-processor.mjs
@@ -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} 条会话`)
diff --git a/packages/boss-auto-browse-and-chat/constant.mjs b/packages/boss-auto-browse-and-chat/constant.mjs
index 20e90f2..c04c3c2 100644
--- a/packages/boss-auto-browse-and-chat/constant.mjs
+++ b/packages/boss-auto-browse-and-chat/constant.mjs
@@ -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="新招呼"]'
diff --git a/packages/boss-auto-browse-and-chat/index.mjs b/packages/boss-auto-browse-and-chat/index.mjs
index a1c887d..f53f4de 100644
--- a/packages/boss-auto-browse-and-chat/index.mjs
+++ b/packages/boss-auto-browse-and-chat/index.mjs
@@ -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)
diff --git a/plan/README.md b/plan/README.md
new file mode 100644
index 0000000..41461a2
--- /dev/null
+++ b/plan/README.md
@@ -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 中补一行条目,避免目录再次难以浏览。*
diff --git a/plan/chat_page_tab_navigation.md b/plan/chat_page_tab_navigation.md
new file mode 100644
index 0000000..51da070
--- /dev/null
+++ b/plan/chat_page_tab_navigation.md
@@ -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` → 不切换(使用当前选中的全部职位)
+- 切换后等待 400–700 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
+```
+
+步骤 5–6 的「强制点击」保证无论上次运行的终止状态如何,都能进入正确的筛选视图,
+且触发 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 架构总览 |
diff --git a/plan/recruiter_architecture.md b/plan/recruiter_architecture.md
index 457b365..f28b622 100644
--- a/plan/recruiter_architecture.md
+++ b/plan/recruiter_architecture.md
@@ -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`,非 ``,以图片代替文字)。
+
+**HTML 骨架(来自 `examples/BOSS直聘-治理公告 (2026_3_26 15:41:51).html`):**
+```html
+
+
+```
+
+**关键选择器(在 `constant.mjs` 中定义):**
+
+| 常量 | 选择器 | 用途 |
+|------|--------|------|
+| `GOVERNANCE_NOTICE_DIALOG_SELECTOR` | `.dialog-uninstall-extension` | 检测弹窗是否存在 |
+| `GOVERNANCE_NOTICE_DIALOG_CONFIRM_BTN_SELECTOR` | `.dialog-uninstall-extension .confirm-btn` | 点击「我已知晓」 |
+
+> **注意:** `confirm-btn` 是 `` 而非 ``,文字由背景图渲染,`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) — 沟通页简历流程详述