mirror of
https://github.com/geekgeekrun/geekgeekrun.git
synced 2026-06-05 07:30:29 +08:00
- 新增 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
223 lines
9.3 KiB
Markdown
223 lines
9.3 KiB
Markdown
# 沟通页 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 架构总览 |
|