diff --git a/.gitignore b/.gitignore index 3ee526c..d2903ee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,26 @@ -node_modules -database.sqlite -.env.local +**/node_modules/ +# build outputs +dist/ +out/ +packages/**/dist/ +packages/**/out/ +packages/**/.vite/ +packages/**/.turbo/ +packages/**/.cache/ + +# OS / logs +.DS_Store +*.log +*.log.* + +# local examples (often large/volatile) +examples/ + +# runtime data / secrets +database.sqlite +*.sqlite +*.sqlite3 +*.db +*.db-journal +.env.local.worktrees diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7a52e3e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,121 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Package Manager + +Use bare `pnpm` (v8 or v10+ both work). The `engines.pnpm` constraint is `>=8.15.9`. + +```bash +pnpm install +pnpm -F geekgeekrun-ui dev +``` + +## Key Commands + +All UI development happens in `packages/ui`. The `dev`/`build`/`start` scripts automatically rebuild `sqlite-plugin` first. + +```bash +# Electron app (main entry point for users) +pnpm -F geekgeekrun-ui dev # development mode +pnpm -F geekgeekrun-ui build # production build +pnpm -F geekgeekrun-ui build:win # Windows installer + +# Lint & format (run from packages/ui) +pnpm -F geekgeekrun-ui lint # eslint --fix +pnpm -F geekgeekrun-ui format # prettier --write + +# Type checking (run from packages/ui) +pnpm -F geekgeekrun-ui typecheck # both node + web + +# SQLite plugin (must build before UI if changed) +pnpm -F @geekgeekrun/sqlite-plugin build +pnpm -F @geekgeekrun/sqlite-plugin dev # watch mode +``` + +## Architecture + +This is a **pnpm monorepo** (`packages/*`) — a desktop automation tool for BOSS Zhipin (job platform) built on Electron + Puppeteer. + +### Two Sides + +**Job-seeker side** (older, more complete): +- `packages/geek-auto-start-chat-with-boss` — core automation: LLM-based resume matching, auto-chat +- `packages/run-core-of-geek-auto-start-chat-with-boss` — headless daemon entry point + +**Recruiter side** (newer, under active development): +- `packages/boss-auto-browse-and-chat` — core automation: browse candidates, filter, send greetings, extract resumes via Canvas hook +- `packages/run-core-of-boss-auto-browse` — headless daemon entry point + +### Electron App (`packages/ui`) + +The app uses **mode-based process routing**: every worker subprocess is actually the same Electron binary launched with a `--mode` flag. `src/main/index.ts` switches on `runMode`: + +- No `--mode` (default) → opens the settings GUI window (`OPEN_SETTING_WINDOW`) +- `bossRecommendMain` / `bossChatPageMain` / `bossAutoBrowseAndChatMain` → recruiter workers +- `geekAutoStartWithBossMain` → job-seeker worker +- `launchDaemon` → background daemon process (manages worker subprocesses via `@geekgeekrun/pm`) + +The GUI renderer is **Vue 3 + Pinia + Vue Router** served by electron-vite. IPC handlers live in `src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts`. + +### Plugin/Hook System + +All automation cores (both sides) use **tapable** (`AsyncSeriesHook`, `AsyncSeriesWaterfallHook`) for extensibility. The sqlite-plugin and webhook features attach to these hooks: + +``` +Worker flow file + → constructs hooks object + → new SqlitePlugin(dbPath).apply(hooks) + → calls core function (startBossAutoBrowse / startBossChatPageProcess / startGeekAutoChat) +``` + +### Shared Packages + +- `packages/sqlite-plugin` — TypeORM + better-sqlite3, compiled TypeScript (`dist/`). Entities: `CandidateInfo`, `CandidateContactLog`, `ChatStartupLog`. **Must be built before UI.** +- `packages/utils` — ESM utilities: sleep, OpenAI/GPT requests, Puppeteer helpers +- `packages/pm` — Electron multi-process daemon/worker management +- `packages/laodeng` / `packages/puppeteer-extra-plugin-laodeng` — anti-bot-detection Puppeteer plugin + +### Storage Layout + +``` +~/.geekgeekrun/ + config/ + boss-recruiter.json # recruiter automation config + candidate-filter.json # candidate filter criteria + webhook.json # webhook integration config + storage/ + boss-cookies.json # persisted BOSS Zhipin cookies + boss-local-storage.json # persisted localStorage + public.db # SQLite database +``` + +### Recruiter Automation Stack (boss-auto-browse-and-chat) + +Key files and their roles: +- `index.mjs` — `startBossAutoBrowse()`: browser launch, login, main loop +- `candidate-processor.mjs` — DOM parsing (`#recommend-list > div > ul > li`), candidate filtering +- `chat-handler.mjs` — clicking 打招呼 (`button.btn-greet`), handling popup (`button.btn-sure-v2`), processCandidate +- `resume-extractor.mjs` — network intercept + iframe Canvas fillText hook (MutationObserver pattern, see `plan/cv_canvas_solution.md`) +- `constant.mjs` — all CSS selectors and URLs; **update here first when BOSS site HTML changes** + +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 ` +
+ + +``` + +**Canvas 简历提取(setupCanvasTextHook):** + +- WASM(Base64+AES-256 解密)在 iframe 内逐字调用 `fillText` 绘制到 `#resume` +- 主页面的 `evaluateOnNewDocument` 不影响 iframe,因此注入方式为: + ``` + 主页面注入 MutationObserver + → 监听 iframe[src*="c-resume"] 创建 + → iframe.addEventListener('load', ...) + → 在 iframe.contentWindow.CanvasRenderingContext2D.prototype 上用 Proxy 包装 fillText + → 收集 { text, x, y } 到主页面 window.__canvasCapturedText + ``` +- 提取后用 `extractResumeText()` 按 y 坐标分行、x 排序拼字 → 返回 `string[]` +- 双层 Canvas 导致每字绘制两次,`extractResumeText` 通过 `Y_THRESHOLD=5px` 分组已自动处理 + +--- + +## 6. 打招呼完成弹窗 + +打招呼成功后,页面弹出"已向牛人发送招呼": + +```html +
+
+
+
已向牛人发送招呼
+
你好,我们公司正在招聘研究员,请问考虑吗
+
+
+ + +
+
+
+``` + +`GREETING_SENT_KNOW_BTN_SELECTOR = 'div.dialog-wrap button.btn-sure-v2'` + +--- + +## 6.5 不感兴趣与原因弹窗 + +对未通过筛选的候选人点击卡片上的"不感兴趣"(`div.tooltip-wrap.suitable`)后,**会先弹出原因选择弹窗**(在 iframe 内),须选一项后弹窗才关闭、该候选人才从列表移除。 + +**弹窗结构(iframe 内,与列表同属 recommendFrame):** + +```html +
+
+
+
+
选择不喜欢的原因,为您优化推荐
+
+ 牛人距离远 + 不考虑本科 + 期望薪资偏高 + + 其他原因 +
+
+
+
...
+
+``` + +**流程:** + +1. 悬停到候选人卡片 → 点击"不感兴趣"(`NOT_INTERESTED_IN_ITEM_SELECTOR`) +2. 等待原因弹窗出现:`NOT_INTERESTED_REASON_POPUP_SELECTOR = 'div.card-reason-f1.show'`(iframe 内) +3. 按**筛选原因**在弹窗内选对应选项(见下表),以便 BOSS 优化推荐;未匹配时选"其他原因" +4. 弹窗关闭,该条从列表移除 + +**筛选原因 → 弹窗选项(constant.mjs `NOT_INTERESTED_REASON_MAP`):** + +| 筛选 reason(candidate-processor) | 弹窗选项文案 | +|-----------------------------------|--------------| +| `city` | 牛人距离远 | +| `education` | 不考虑本科 | +| `salary` | 期望薪资偏高 | +| `workExp` | 工作经历和制剂研发无关 | +| `viewed` | 重复推荐 | +| `skills` / `blockName` | 优先选文案包含"与职位不符"的选项(如"期望(xxx)与职位不符"),否则"其他原因" | +| 其他/未知 | 其他原因 | + +**选择器与常量(constant.mjs):** + +- `NOT_INTERESTED_IN_ITEM_SELECTOR`:卡片内"不感兴趣"区域 +- `NOT_INTERESTED_REASON_POPUP_SELECTOR`:原因弹窗容器 +- `NOT_INTERESTED_REASON_ITEMS_SELECTOR`:弹窗内所有 `span.first-reason-item`,按文案匹配 +- `NOT_INTERESTED_REASON_MAP`:reason → 弹窗文案 +- `NOT_INTERESTED_REASON_POSITION_MISMATCH`:用于 skills/blockName 的包含匹配("与职位不符") +- `NOT_INTERESTED_REASON_FALLBACK`:无匹配时使用("其他原因") + +**扩展(后续可做):** 可将 `filterResult.reasonDetail` 或候选人信息交给 LLM,由 LLM 返回更贴切的弹窗选项文案,再在 `span.first-reason-item` 中做模糊匹配点击,进一步优化 BOSS 推荐效果。 + +--- + +## 7. 每日限额检测(checkDailyLimit) + +通过 `document.body.innerText` 匹配文字特征判断,无独立 API: + +``` +/今日已沟通\s*(\d+)\s*\/\s*(\d+)/ → count/max 数字解析 +/今日沟通人数已达上限|明天再来|今日.*已达上限/ → 直接判定 limitReached=true +``` + +风控检测(在 `startChatWithCandidate` 点击打招呼后立即检测): + +``` +/风控|存在风险|请稍后再试|操作过于频繁/ → reason: RISK_CONTROL +``` + +--- + +## 8. 滚动加载(scrollAndLoadMore) + +推荐牛人页为**无限滚动**,无分页按钮。滚动使用 `page.mouse.wheel()` 模拟人工: + +``` +maxScrolls = 3, stepsPerScroll = 4 +每步:deltaY = 25~40px,间隔 80~160ms +``` + +到底判断: +1. `document.body.innerText` 含 `/没有更多|已经到底|加载完毕|暂无更多/` +2. Vue `el.__vue__?.hasMore === false` + +两者均为"没有更多"时返回 `false`,主循环退出。 + +--- + +## 9. 反检测机制 + +- **puppeteer-extra-plugin-stealth** — 抹除 headless 特征 +- **@geekgeekrun/puppeteer-extra-plugin-laodeng** — 自定义指纹混淆 +- **puppeteer-extra-plugin-anonymize-ua** — 随机 UserAgent(`makeWindows: false`) +- **ghost-cursor** — `createHumanCursor(page)` 所有点击走人类轨迹,不用 `page.click()` +- **sleepWithRandomDelay** — 操作间随机延迟 +- Canvas hook 用 **Proxy**(不直接替换 prototype),`fillText.toString()` 仍返回 `[native code]` + +--- + +## 10. 配置文件 + +**boss-recruiter.json**(`~/.geekgeekrun/config/boss-recruiter.json`): +```json +{ + "logLevel": "info", + "targetJobId": "", + "recommendPage": { + "clickNotInterestedForFiltered": true, + "skipViewedCandidates": false, + "runOnceAfterComplete": false + }, + "autoChat": { + "greetingMessage": "你好,...", + "maxChatPerRun": 50, + "delayBetweenChats": [3000, 8000] + } +} +``` +- `logLevel`:日志级别 `"debug"` | `"info"` | `"warn"` | `"error"`;设为 `"debug"` 时每次自动操作(解析、悬停、点击不感兴趣、选原因等)都会打印日志,便于排查。 +- `recommendPage.clickNotInterestedForFiltered`:对未通过筛选的候选人点击"不感兴趣"(卡片内 `div.tooltip-wrap.suitable`),并自动在原因弹窗中选择一项(默认"其他原因")后关闭,避免重复扫描;设为 `false` 可关闭便于调试。 +- `recommendPage.skipViewedCandidates`:是否跳过已读候选人(卡片带 `has-viewed` 的项);与 candidate-filter 的 `skipViewedCandidates` 一致,以 boss-recruiter 为准。 +- `recommendPage.runOnceAfterComplete`:单次运行结束后是否停止(不再次启动);`true` 时 worker 只跑一轮后退出。 +- `recommendPage.delayBetweenNotInterestedMs`:每次点击"不感兴趣"(含原因选择)之间的延迟 [min, max] 毫秒,在此区间随机,用于反检测;默认 `[800, 2500]`。 +- `recommendPage.keepBrowserOpenAfterRun`:单轮结束后是否保持浏览器打开(仅招聘端推荐页,不影响应聘端;需同时开启 runOnceAfterComplete);开启时关闭浏览器窗口后 worker 再退出。 + +**candidate-filter.json**(`~/.geekgeekrun/config/candidate-filter.json`): +```json +{ + "expectCityList": ["北京", "上海"], + "expectEducationList": ["本科", "硕士"], + "expectWorkExpRange": [1, 5], + "expectSalaryRange": [15, 50], + "expectSalaryWhenNegotiable": "exclude", + "expectSkillKeywords": ["Vue", "React"], + "blockCandidateNameRegExpStr": "测试|内推", + "skipViewedCandidates": false +} +``` +筛选未通过时日志会打印原因,`reason` 可能为:`city` / `education` / `workExp` / `salary` / `skills` / `blockName` / `viewed`。 + +`expectSalaryRange` 单位:千/月(K)。配置可填 K 或元:若数值 ≥100 会按元自动折成 K(如 8000→8K)。薪资解析:`parseSalaryRange()` 支持 `3-5K`、`10-15K`、`20-30K`(万自动 ×10)。 +`expectSalaryWhenNegotiable`:候选人薪资为"面议"或无法解析时 — `exclude`(默认)= 不通过,`include` = 通过(不因薪资排除)。 + +--- + +## 11. Hooks(推荐页用到的) + +| Hook | 参数 | 触发时机 | +|------|------|---------| +| `beforeBrowserLaunch` | — | 浏览器启动前 | +| `afterBrowserLaunch` | — | 浏览器启动后、导航前 | +| `beforeNavigateToRecommend` | — | Tab1/Tab2 导航前 | +| `onCandidateListLoaded` | — | 登录成功、列表可用时(触发 GUI progress fulfilled) | +| `onCandidateFiltered` | `(candidates, filterResult)` | 每轮筛选完成(waterfall,可修改结果) | +| `beforeStartChat` | `(candidate)` | 开始处理单个候选人前 | +| `afterChatStarted` | `(candidate, chatResult)` | 打招呼完成后(成功或失败) | +| `onProgress` | `{ phase:'recommend', current, max }` | 每次打招呼成功后 | +| `onError` | `(error)` | 主循环抛出异常 | +| `onComplete` | — | 主循环正常结束 | diff --git a/plan/recruiter_architecture.md b/plan/recruiter_architecture.md new file mode 100644 index 0000000..f28b622 --- /dev/null +++ b/plan/recruiter_architecture.md @@ -0,0 +1,535 @@ +# 招聘端(Recruiter/BOSS)架构总览 + +> **定位**:供 AI Agent 快速理解招聘端全貌,用于协作开发时减少 token 消耗。 +> 最后更新:2026-03-26 + +--- + +## 1. 整体架构一览 + +``` +UI 层 (Electron Renderer) + └── 四个配置/启动页面 + ├── BossAutoBrowseAndChat → 推荐牛人:配置 + 启动 + ├── BossChatPage → 沟通页:配置 + 启动 + ├── BossAutoSequence → 顺序执行(无独立配置,复用上两者) + └── WebhookIntegration → Webhook / 外部集成(配置 + 测试发送) + +IPC 层 (Electron Main) + └── ipc/index.ts → run / stop / save / fetch-config 处理器 + +Worker 层 (独立 Electron 子进程,通过 daemon 管理) + ├── bossRecommendMain → flow/BOSS_RECOMMEND_MAIN/index.ts + ├── bossChatPageMain → flow/BOSS_CHAT_PAGE_MAIN/index.ts + └── bossAutoBrowseAndChatMain → flow/BOSS_AUTO_BROWSE_AND_CHAT_MAIN/index.ts(串联上两者) + +自动化核心 (packages/boss-auto-browse-and-chat/) + ├── index.mjs → startBossAutoBrowse(推荐牛人主入口) + ├── chat-page-processor.mjs → startBossChatPageProcess(沟通页主入口) + ├── candidate-processor.mjs → 解析列表、筛选候选人、滚动加载 + ├── chat-handler.mjs → 点击打招呼、发送招呼语、检查日限 + ├── resume-extractor.mjs → 网络拦截 + Canvas hook 提取简历文本 + ├── chat-page-resume.mjs → 在线简历、附件简历、下载 PDF + ├── data-manager.mjs → 查重、保存候选人信息、写联系日志 + ├── humanMouse.mjs → ghost-cursor 人类鼠标模拟 + ├── constant.mjs → URL 常量、DOM 选择器 + └── runtime-file-utils.mjs → 读写 config / storage 文件工具 + +持久化 (@geekgeekrun/sqlite-plugin) + └── SQLite: ~/.geekgeekrun/storage/public.db + ├── CandidateInfo → 候选人基础信息 + └── CandidateContactLog → 联系记录日志 + +外部集成 (packages/ui/src/main/features/webhook/) + └── index.ts → sendWebhook / buildMockPayload + 配置文件:~/.geekgeekrun/config/webhook.json(boss-auto-browse 路径) +``` + +--- + +## 2. 三个 Worker 说明 + +| Worker ID | Flow 文件 | 调用的核心函数 | 对应页面 | +|-----------|-----------|--------------|--------| +| `bossRecommendMain` | `BOSS_RECOMMEND_MAIN/index.ts` | `startBossAutoBrowse(hooks)` | BossAutoBrowseAndChat | +| `bossChatPageMain` | `BOSS_CHAT_PAGE_MAIN/index.ts` | `startBossChatPageProcess(hooks)` | BossChatPage | +| `bossAutoBrowseAndChatMain` | `BOSS_AUTO_BROWSE_AND_CHAT_MAIN/index.ts` | 两者串联,先推荐后沟通,browser 在两阶段间复用 | BossAutoSequence | + +**Worker 生命周期(通用模式):** +1. `initPuppeteer()` 注册 stealth/laodeng/anonymize-ua 插件 +2. 构建 `hooks` 对象(AsyncSeriesHook / AsyncSeriesWaterfallHook) +3. `new SqlitePlugin(dbPath).apply(hooks)` 挂载 DB 操作 +4. 无限循环:执行主函数 → 出错则等待 `rerunInterval`(默认 3000ms)重试 +5. 特定错误(LOGIN_STATUS_INVALID / ERR_INTERNET_DISCONNECTED 等)直接 `process.exit(exitCode)` +6. 通过 `sendToDaemon()` 向 GUI 发送进度消息 + +--- + +## 3. 推荐牛人(Recommend Page)主循环 + +``` +startBossAutoBrowse(hooks, { returnBrowser? }) + 1. hooks.beforeBrowserLaunch + 2. 启动 Puppeteer 浏览器 + 3. Tab1 (pageChat) → BOSS_CHAT_INDEX_URL(沟通页,当前仅作展示) + 4. Tab2 (page) → BOSS_RECOMMEND_PAGE_URL(推荐牛人,主循环在此) + 5. 注入 Cookie + localStorage(boss-cookies.json / boss-local-storage.json) + 6. 登录检测:URL 未在推荐页则等待用户手动登录后持久化存储 + 7. hooks.onCandidateListLoaded(触发 GUI 登录状态 fulfilled) + 8. 主循环: + while (hasMore) { + candidates = parseCandidateList(page) // DOM/Vue 解析 + filtered = filterCandidates(candidates, cfg) // 城市/学历/工作年限/薪资/技能/屏蔽名 + hooks.onCandidateFiltered(filtered) + for each matched: + checkDailyLimit(page) // 今日已达上限则 break + if count >= maxChatPerRun: break + processCandidate(page, candidate, ...) // 含打招呼延迟 + hooks.onProgress({ phase:'recommend', current, max }) + hasMore = scrollAndLoadMore(page) + } + 9. returnBrowser ? return { browser, page } : browser.close() +``` + +--- + +## 4. 沟通页(Chat Page)主流程 + +``` +startBossChatPageProcess(hooks, { browser?, page? }) + 1. 若有传入 browser/page 则复用(来自顺序执行),否则自行启动 + 2. 导航到 BOSS_CHAT_PAGE_URL + 3. 解析左侧会话列表,筛选 unread === true 的会话 + 4. 每条会话(最多 maxProcessPerRun): + a. checkIfAlreadyContacted(hooks) → 已接触则 skip + b. 点击会话(human cursor) + c. 若存在附件简历消息 → openPreviewAndDownloadPdf() + d. 否则若候选人已发招呼 → + openOnlineResume() → getOnlineResumeText()(Canvas hook) + mode=keywords: 关键词 substring 匹配 + mode=llm: screenCandidateWithLlm(text, rule) → JSON { pass, reason } + pass → requestAttachmentResume() + e. saveCandidateInfo() + logContact() + f. hooks.onProgress({ phase:'chatPage', current, max }) +``` + +--- + +## 5. 配置文件结构 + +**路径:** `~/.geekgeekrun/config/` + +### webhook.json(boss-auto-browse 配置路径) + +```json +{ + "enabled": true, + "url": "https://your-paperless.example.com/api/documents/post_document/", + "method": "POST", + "headers": { + "Authorization": "Token YOUR_TOKEN" + }, + "payloadOptions": { + "includeBasicInfo": true, + "includeFilterReason": true, + "includeLlmConclusion": true, + "includeResume": "path" + } +} +``` + +`includeResume` 取值:`"none"` | `"path"` | `"base64"` + +**Payload 结构(发出的 JSON):** +```json +{ + "runId": "run-", + "timestamp": "ISO8601", + "summary": { "total": 10, "matched": 7, "skipped": 3 }, + "candidates": [ + { + "basicInfo": { "name": "...", "education": "...", "workExpYears": 3, "city": "...", "salary": "...", "skills": [] }, + "filterReport": { "matched": true, "matchedRules": [], "score": 85 }, + "llmConclusion": "...", + "resumeFile": { "path": "/abs/path/to/resume.pdf", "filename": "张三.pdf" } + } + ] +} +``` + +**自动触发时机:** `bossAutoBrowseAndChatMain` worker 中每轮(推荐页 + 沟通页)执行完毕后,读取 webhook.json,若 `enabled=true` 则发送汇总报告;失败不影响主流程,清空 `sessionCandidates` 后等待下轮。 + +**候选人数据来源:** `afterChatStarted` hook(每次打招呼后触发),收集 `candidate` 对象中的 `info`、`matchedRules`、`score`、`llmConclusion`、`resumeFilePath`、`resumeFileName` 字段。 + +--- + +### boss-recruiter.json +```json +{ + "targetJobId": "", + "autoChat": { + "greetingMessage": "你好,...", + "maxChatPerRun": 50, + "delayBetweenChats": [3000, 8000] + }, + "chatPage": { + "maxProcessPerRun": 20, + "filter": { + "mode": "keywords", + "keywordList": ["Python", "机器学习"], + "llmRule": "" + } + } +} +``` + +### candidate-filter.json +```json +{ + "expectCityList": ["北京", "上海"], + "expectEducationList": ["本科", "硕士"], + "expectWorkExpRange": [1, 5], + "expectSalaryRange": [15, 50], + "expectSkillKeywords": ["Vue", "React"], + "blockCandidateNameRegExpStr": "测试|内推" +} +``` + +**路径:** `~/.geekgeekrun/storage/` + +- `boss-cookies.json` — 招聘端 Cookie +- `boss-local-storage.json` — 域名 localStorage +- `public.db` — SQLite 数据库 + +--- + +## 6. 关键常量(constant.mjs) + +> 以下为主要常量摘录,完整列表以源文件为准。BOSS 站点改版时常量可能失效,参见 §14.5 排查流程。 + +```js +// URL +BOSS_RECOMMEND_PAGE_URL = 'https://www.zhipin.com/web/chat/recommend' +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 = '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.button-chat' +CHAT_INPUT_SELECTOR = '#boss-chat-global-input' + +// 沟通页选择器(CHAT_PAGE_* 前缀) +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' +``` + +--- + +## 7. IPC 通信接口(main ↔ renderer) + +### 招聘端专用 IPC + +| Channel | 方向 | 说明 | +|---------|------|------| +| `run-boss-recommend` | invoke | 启动推荐牛人 worker,返回 `{ runRecordId }` | +| `stop-boss-recommend` | invoke | 停止推荐牛人 worker(等待退出) | +| `run-boss-chat-page` | invoke | 启动沟通页 worker,返回 `{ runRecordId }` | +| `stop-boss-chat-page` | invoke | 停止沟通页 worker | +| `run-boss-auto-browse-and-chat` | invoke | 启动顺序执行 worker,返回 `{ runRecordId }` | +| `stop-boss-auto-browse-and-chat` | invoke | 停止顺序执行 worker | +| `save-boss-recruiter-config` | invoke | 保存招聘端配置(JSON 字符串),同时写 boss-recruiter.json 和 candidate-filter.json | +| `fetch-boss-recruiter-config-file-content` | invoke | 读取配置,返回 `{ config: { 'boss-recruiter.json': {}, 'candidate-filter.json': {} } }` | +| `check-boss-recruiter-cookie-file` | invoke | 检查 Cookie 格式是否合法 | +| `fetch-webhook-config` | invoke | 读取 webhook.json,返回配置对象或 null | +| `save-webhook-config` | invoke | 写入 webhook.json(接收 JSON 字符串) | +| `test-webhook` | invoke | 发送 mock payload 到已配置的 URL,返回 `{ status, body }` | +| `trigger-webhook-manually` | invoke | 用 mock payload(标记为 manual)手动触发一次,返回 `{ status, body }` | + +### Worker → GUI 消息(通过 daemon) + +消息类型(`data.type` 字段): + +| type | 说明 | +|------|------| +| `worker-log` | 普通日志文本 | +| `prerequisite-step-by-step-checkstep-by-step-check` | 前置步骤状态更新,含 `{ step: { id, status } }` | +| `boss-auto-browse-progress` | 进度更新,含 `{ phase, current, max }` | +| `worker-exited` | Worker 进程退出 | + +--- + +## 8. 前置步骤(RunningOverlay 进度条) + +``` +getBossAutoBrowseSteps() → [ + { id: 'worker-launch', describe: '启动子进程' }, + { id: 'puppeteer-executable-check', describe: 'Puppeteer 可执行程序检查' }, + { id: 'login-status-check', describe: '登录状态检查(若浏览器弹出请以招聘者身份登录)' } +] +``` + +步骤状态:`'todo' | 'pending' | 'fulfilled' | 'rejected'` + +--- + +## 9. Hooks 体系 + +所有 worker 都构造相同结构的 hooks 传给核心模块,SqlitePlugin 在上面挂载 DB 操作: + +```ts +const hooks = { + beforeBrowserLaunch: AsyncSeriesHook, + afterBrowserLaunch: AsyncSeriesHook, + beforeNavigateToRecommend: AsyncSeriesHook, + onCandidateListLoaded: AsyncSeriesHook, + onCandidateFiltered: AsyncSeriesWaterfallHook, // ['candidates', 'filterResult'] + beforeStartChat: AsyncSeriesHook, // ['candidate'] + afterChatStarted: AsyncSeriesHook, // ['candidate', 'result'] + onError: AsyncSeriesHook, // ['error'] + onComplete: AsyncSeriesHook, + onProgress: AsyncSeriesHook, // ['payload'] + // SqlitePlugin 额外挂载: + createOrUpdateCandidateInfo: AsyncSeriesHook + insertCandidateContactLog: AsyncSeriesHook + queryCandidateByEncryptId: AsyncSeriesWaterfallHook +} +``` + +--- + +## 10. 反检测机制 + +- **puppeteer-extra-plugin-stealth** — 抹除 headless 特征 +- **@geekgeekrun/puppeteer-extra-plugin-laodeng** — 自定义反检测 +- **puppeteer-extra-plugin-anonymize-ua** — 随机 UserAgent(`makeWindows: false`) +- **ghost-cursor** — `createHumanCursor(page)` 模拟人类鼠标轨迹,所有点击走 cursor 而非 `page.click()` +- **sleepWithRandomDelay(base, range)** — 操作间随机延迟 +- 滚动:`page.mouse.wheel({ deltaY })` 分步骤随机延迟 + +--- + +## 11. 路由与导航 + +| 路由 Name | 路径 | 页面组件 | +|----------|------|--------| +| `BossAutoBrowseAndChat` | `/main-layout/BossAutoBrowseAndChat` | 推荐牛人 - 自动开聊 | +| `BossChatPage` | `/main-layout/BossChatPage` | 沟通页 | +| `BossAutoSequence` | `/main-layout/BossAutoSequence` | 自动顺序执行 | +| `WebhookIntegration` | `/main-layout/WebhookIntegration` | Webhook / 外部集成 | + +切换到招聘端身份时默认重定向到 `BossAutoBrowseAndChat`。 + +`RECRUITER_ROUTES = ['BossAutoBrowseAndChat', 'BossChatPage', 'BossAutoSequence']` 用于判断当前身份模式。 +> 注:`WebhookIntegration` 是纯配置页,无需加入 `RECRUITER_ROUTES`(无 RunningOverlay)。 + +--- + +## 12. 如何新增招聘端页面 + +1. **新建页面组件** + ``` + packages/ui/src/renderer/src/page/MainLayout/Boss/index.vue + ``` + - 若需要启动任务:参考 `BossAutoBrowseAndChat/index.vue` 或 `BossChatPage/index.vue` + - 使用 `RunningOverlay` 组件显示进度: + ```vue + + ``` + +2. **注册路由** — 在 `packages/ui/src/renderer/src/router/index.ts` 的 `/main-layout` children 中添加: + ```ts + { name: 'Boss', path: 'Boss', component: () => import(...), meta: { title: '...' } } + ``` + +3. **更新 RECRUITER_ROUTES** — 在 `packages/ui/src/renderer/src/page/MainLayout/index.vue` 中: + ```ts + const RECRUITER_ROUTES = ['BossAutoBrowseAndChat', 'BossChatPage', 'BossAutoSequence', 'Boss'] + ``` + +4. **添加导航入口** — 在 `packages/ui/src/renderer/src/page/MainLayout/LeftNavBar/RecruiterPart.vue`: + ```vue + 页面名称 + ``` + +5. **(如需新 Worker)新建 flow 文件** + ``` + packages/ui/src/main/flow/BOSS__MAIN/index.ts + ``` + - 复制 `BOSS_RECOMMEND_MAIN/index.ts` 为模板 + - 修改 `workerId`、调用的核心函数、日志前缀 + +6. **注册新 mode** — 在 `packages/ui/src/main/index.ts` 的 switch 中: + ```ts + case 'bossMain': { + const { waitForProcessHandShakeAndRunAutoChat } = await import('./flow/BOSS__MAIN/index') + waitForProcessHandShakeAndRunAutoChat() + break + } + ``` + +7. **添加 IPC 处理器** — 在 `packages/ui/src/main/flow/OPEN_SETTING_WINDOW/ipc/index.ts`: + ```ts + ipcMain.handle('run-boss-', async () => { + const mode = 'bossMain' + const { runRecordId } = await runCommon({ mode }) + daemonEE.on('message', function handler(message) { + if (message.workerId !== mode) return + if (message.type === 'worker-exited') mainWindow?.webContents.send('worker-exited', message) + }) + return { runRecordId } + }) + ipcMain.handle('stop-boss-', async () => { + // 参考 stop-boss-recommend 的模式 + }) + ``` + +8. **(如需新配置字段)更新 save-boss-recruiter-config 处理器**(同文件) + +--- + +## 13. 如何在核心模块新增功能 + +### 新增候选人筛选条件 +- 在 `candidate-processor.mjs` 的 `filterCandidates()` 添加判断逻辑 +- 在 `candidate-filter.json` 对应结构增加字段 +- 在 `BossAutoBrowseAndChat/index.vue` 增加 UI 表单项 +- 在 IPC `save-boss-recruiter-config` 中处理新字段的保存/读取 + +### 新增 Hook 钩子点 +- 在 worker flow 文件的 `hooks` 对象中添加 `new AsyncSeriesHook(['...'])` +- 在核心 .mjs 文件对应位置调用 `await hooks.newHook?.promise?.(payload)` + +### 新增联系日志类型 +- 在 `data-manager.mjs` 的 `logContact()` 调用时传入新的 `contactType` 字符串 +- SQLite Plugin 会自动持久化 + +--- + +## 14. 已知弹窗及自动处理清单 + +BOSS直聘在各页面会弹出各类提示/公告弹窗,均需自动点击关闭,否则会遮挡操作区域或导致自动化卡死。以下列出所有已纳入代码处理的弹窗。 + +--- + +### 14.1 治理公告弹窗(dialog-uninstall-extension) + +**何时出现:** 每次登录后(包含首次加载、cookie 失效重新登录),浏览器导航到 BOSS 站点后必现。BOSS 借此告知平台禁止使用第三方自动化工具。 + +**外观:** 全屏遮罩,正中宽 580px 卡片,含平台公告文字;底部有一枚背景图模拟的「我已知晓」按钮(`div.confirm-btn`,非 `