├── 预览图 ├── .keep ├── 智能聊天面板.png └── 自动沟通面板.png ├── README.md ├── README.en.md ├── LICENSE └── Boss_helper.js /预览图/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /预览图/智能聊天面板.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YangShengzhou03/Jobs_helper/HEAD/预览图/智能聊天面板.png -------------------------------------------------------------------------------- /预览图/自动沟通面板.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/YangShengzhou03/Jobs_helper/HEAD/预览图/自动沟通面板.png -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🚀 BOSS海投助手 (BOSS Helper) 🌟 4 | 5 | [![AGPL-3.0 License](https://img.shields.io/badge/License-AGPL_v3-blue.svg?style=for-the-badge&logo=gnu)](https://www.gnu.org/licenses/agpl-3.0) 6 | [![GitHub Stars](https://img.shields.io/github/stars/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=github)](https://github.com/YangShengzhou03/Jobs_helper) 7 | [![GitHub Forks](https://img.shields.io/github/forks/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=github)](https://github.com/YangShengzhou03/Jobs_helper) 8 | [![GitHub Issues](https://img.shields.io/github/issues/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=github)](https://github.com/YangShengzhou03/Jobs_helper/issues) 9 | [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=github)](https://github.com/YangShengzhou03/Jobs_helper/pulls) 10 | [![Last Commit](https://img.shields.io/github/last-commit/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=git)](https://github.com/YangShengzhou03/Jobs_helper) 11 | 12 | [![JavaScript](https://img.shields.io/badge/JavaScript-ES6+-yellow?style=flat-square&logo=javascript)](https://developer.mozilla.org/en-US/docs/Web/JavaScript) 13 | [![Tampermonkey](https://img.shields.io/badge/Tampermonkey-8.15+-green?style=flat-square&logo=tampermonkey)](https://www.tampermonkey.net/) 14 | [![Chrome](https://img.shields.io/badge/Chrome-88+-blue?style=flat-square&logo=google-chrome)](https://www.google.com/chrome/) 15 | [![Firefox](https://img.shields.io/badge/Firefox-85+-orange?style=flat-square&logo=firefox-browser)](https://www.mozilla.org/firefox/) 16 | 17 |
18 | 19 | --- 20 | 21 |
22 | 23 | **🌟 如果这个项目帮助到了您,请给个Star支持我一下!** 24 | 25 | [![Star History Chart](https://api.star-history.com/svg?repos=YangShengzhou03/Jobs_helper&type=Date)](https://star-history.com/#YangShengzhou03/Jobs_helper&Date) 26 | 27 |
28 | 29 | --- 30 | 31 | ## 📖 项目概览 32 | 33 | **BOSS海投助手**是一款专为求职者设计的电脑浏览器脚本,目的是希望能提升在[BOSS直聘平台](https://www.zhipin.com/)上的求职沟通效率和投递速度。通过自动化操作、AI辅助回复等功能,帮助用户快速筛选合适岗位并完成简历投递与消息回复操作。 34 | 35 | ### 🎯 核心特性 36 | 37 | - 📊 **自动批量投递** - 筛选并自动投递岗位,发送图片简历等 38 | - 🎯 **多维度精准筛选** - 关键词、地区和HR在线时间等多条件过滤 39 | - 💬 **AI智能回复** - 基于大语言模型生成自然、专业的消息回复 40 | - 🎨 **现代化控制面板** - 可视化操作界面,实时监控任务状态 41 | - 🔄 **重复机制** - 自动识别已投递岗位,避免重复操作 42 | 43 | ## 🛠️ 技术架构 44 | 45 | ### 系统架构 46 | 47 | ``` 48 | BOSS海投助手架构 49 | ├── 📦 核心模块 (Core) 50 | │ ├── 自动化投递引擎 51 | │ ├── 页面解析器 52 | │ ├── AI回复处理器 53 | │ └── 状态管理机 54 | ├── 🎨 UI模块 (UI) 55 | │ ├── 控制面板系统 56 | │ ├── 主题管理系统 57 | │ └── 交互反馈组件 58 | ├── 💾 数据模块 (State) 59 | │ ├── 本地存储管理 60 | │ ├── 会话状态维护 61 | │ └── 配置持久化 62 | ├── 🔧 工具模块 (Utils) 63 | │ ├── DOM操作工具 64 | │ ├── 异步处理工具 65 | │ └── 错误处理系统 66 | └── ⚙️ 配置模块 (Config) 67 | ├── 运行时配置 68 | ├── 选择器配置 69 | └── 常量定义 70 | ``` 71 | 72 | ### 技术栈 73 | 74 | | 技术领域 | 具体技术 | 版本要求 | 75 | |---------|---------|---------| 76 | | **核心语言** | JavaScript (ES6+) | ES2015+ | 77 | | **脚本引擎** | Tampermonkey / ScriptCat | 8.15+ | 78 | | **浏览器支持** | Chrome, Firefox, Edge(推荐) | 最新版 | 79 | | **AI集成** | 讯飞星火API / OpenAI API | - | 80 | | **数据存储** | localStorage, IndexedDB | - | 81 | | **构建工具** | 原生JS,无其他依赖 | - | 82 | 83 | ## 📦 安装指南 84 | 85 | ### 前置要求 86 | 87 | 1. **浏览器扩展** - 安装以下任一脚本管理器: 88 | - [Tampermonkey](https://www.tampermonkey.net/) (推荐) 89 | - [ScriptCat(脚本猫)](https://scriptcat.org/) 90 | 91 | 2. **浏览器版本** - 支持现代浏览器: 92 | - Chrome 88+ 93 | - Firefox 85+ 94 | - Edge 88+ 95 | - Safari 14+ 96 | 97 | ### 安装步骤 98 | 99 | #### 方法一:一键安装(推荐) 100 | 101 | 点击右侧链接安装: [![安装脚本](https://img.shields.io/badge/Install-Script-green?style=for-the-badge)](https://raw.githubusercontent.com/YangShengzhou03/Jobs_helper/refs/heads/Boss/Boss_helper.js) 102 | 103 | #### 方法二:手动安装 104 | 105 | 1. 访问项目GitHub页面:https://github.com/YangShengzhou03/Jobs_helper 106 | 2. 下载 `Boss_helper.js` 文件 107 | 3. 在脚本管理器中点击"新建脚本" 108 | 4. 粘贴文件内容并保存 109 | 5. 刷新BOSS直聘页面即可使用 110 | 111 | ## 🚀 快速开始 112 | 113 | ### 1. 登录BOSS直聘 114 | 115 | 确保已登录您的BOSS直聘账号 116 | 117 | ### 2. 开始前配置 118 | 119 | - **将常用语修改为自我介绍**: [常用语设置](https://www.zhipin.com/web/geek/notify-set?ka=notify-set) 120 | - **必须启用招呼语功能**: [启用打招呼](https://www.zhipin.com/web/geek/notify-set?type=greetSet) 121 | 122 | ### 3. 访问支持页面 123 | 124 | - **职位列表页**: https://www.zhipin.com/web/geek/jobs 125 | - **聊天对话页**: https://www.zhipin.com/web/geek/chat 126 | 127 | ### 4. 配置筛选条件 128 | 129 | 在控制面板中设置: 130 | - ✅ 职位关键词(如:前端、Java、Python) 131 | - ✅ 工作地点(如:北京、杭州、深圳) 132 | - ✅ 薪资范围筛选 133 | - ✅ 公司类型过滤 134 | 135 | ### 5. 启动自动化 136 | 137 | 点击"开始投递"按钮,系统将自动: 138 | 1. 扫描并筛选符合条件的岗位 139 | 2. 自动进入每个职位详情页 140 | 3. 点击"立即沟通"按钮 141 | 4. 发送预设的自我介绍消息 142 | 5. 记录所有操作日志 143 | 144 | ## 🎯 功能详解 145 | 146 | ### 🤖 自动化投递系统 147 | 148 | | 功能模块 | 描述 | 技术实现 | 149 | |---------|------|---------| 150 | | **岗位扫描** | 自动滚动加载所有职位列表 | `MutationObserver` + 智能滚动检测 | 151 | | **条件筛选** | 多维度精准匹配目标岗位 | 正则匹配 + 语义分析 | 152 | | **自动沟通** | 模拟点击立即沟通按钮 | DOM事件模拟 + 异步等待 | 153 | | **防重复机制** | 识别已处理过的HR和岗位 | localStorage + 哈希标识 | 154 | 155 | ### 💬 AI智能回复系统 156 | 157 | ```javascript 158 | // AI回复处理流程 159 | async function handleAIReply(hrMessage) { 160 | // 1. 消息预处理 161 | const cleanedMessage = preprocessMessage(hrMessage); 162 | 163 | // 2. 意图识别 164 | const intent = await detectIntent(cleanedMessage); 165 | 166 | // 3. 生成回复 167 | const reply = await generateReply(intent, cleanedMessage); 168 | 169 | // 4. 发送回复 170 | await sendChatMessage(reply); 171 | } 172 | ``` 173 | 174 | ### 🎨 控制面板功能 175 | 176 | - **实时状态监控** - 显示当前处理进度和统计信息 177 | - **动态配置调整** - 实时修改筛选条件和操作参数 178 | - **主题切换** - 支持亮色/暗色主题模式 179 | - **日志查看器** - 实时显示操作日志和错误信息 180 | - **性能监控** - 显示内存使用和运行时间统计 181 | 182 | ## ⚙️ 配置说明 183 | 184 | ### 基本配置 185 | 186 | ```javascript 187 | // config.js - 主要配置项 188 | const CONFIG = { 189 | BASIC_INTERVAL: 1000, // 基础操作间隔(ms) 190 | OPERATION_INTERVAL: 800, // 具体操作间隔(ms) 191 | MAX_REPLIES_FREE: 5, // 免费版AI回复次数 192 | MAX_REPLIES_PREMIUM: 10, // 高级版AI回复次数 193 | DEFAULT_AI_ROLE: '求职者角色设定', // AI默认人设 194 | }; 195 | ``` 196 | 197 | ### AI配置 198 | 199 | 在脚本设置中配置AI服务: 200 | - 讯飞星火API密钥 201 | - OpenAI API密钥(可选) 202 | - 自定义回复模板 203 | - 角色设定配置 204 | 205 | ### 筛选条件配置 206 | 207 | 支持多种筛选条件组合: 208 | - 包含/排除关键词 209 | - 地理位置范围 210 | - 薪资水平区间 211 | - 公司规模筛选 212 | - 行业类型过滤 213 | 214 | ## 📊 性能指标 215 | 216 | ### 处理效率 217 | 218 | | 指标 | 数值 | 说明 | 219 | |------|------|------| 220 | | 平均处理速度 | 2-3秒/岗位 | 从扫描到完成沟通 | 221 | | 最大并发数 | 1个/标签页 | 单标签页处理 | 222 | | 每日处理上限 | 50个岗位 | 防滥用机制 | 223 | | 内存占用 | <10MB | 轻量级设计 | 224 | 225 | ### 成功率统计 226 | 227 | | 操作类型 | 成功率 | 备注 | 228 | |----------|--------|------| 229 | | 岗位扫描 | 99.8% | 极少数页面结构变化 | 230 | | 自动沟通 | 98.5% | 依赖页面加载速度 | 231 | | AI回复 | 95.2% | 受网络和API限制 | 232 | | 简历发送 | 97.3% | 需要HR先回复 | 233 | 234 | ## 🔧 开发指南 235 | 236 | ### 项目结构 237 | 238 | ``` 239 | jobs-helper/ 240 | ├── 📄 Boss_helper.js # 主入口文件 241 | ├── 📄 config.js # 配置常量 242 | ├── 📄 core.js # 核心业务逻辑 243 | ├── 📄 ui.js # 用户界面组件 244 | ├── 📄 state.js # 状态管理 245 | ├── 📄 utils.js # 工具函数 246 | ├── 📄 letter.js # 引导消息 247 | ├── 📄 guide.js # 用户引导 248 | ├── 📄 settings.js # 设置面板 249 | └── 📄 README.md # 项目说明 250 | ``` 251 | 252 | ### 开发环境搭建 253 | 254 | ```bash 255 | # 1. 克隆项目 256 | git clone https://github.com/YangShengzhou03/Jobs_helper.git 257 | 258 | # 2. 安装依赖(无需构建,直接使用) 259 | # 本项目为纯前端项目,无构建依赖 260 | 261 | # 3. 开发调试 262 | # 使用浏览器开发者工具进行调试 263 | # 推荐使用Tampermonkey的调试模式 264 | ``` 265 | 266 | ### 代码贡献 267 | 268 | 欢迎提交Pull Request!请遵循以下规范: 269 | 270 | 1. **代码风格** - 遵循ES6+语法规范 271 | 2. **注释要求** - 重要函数必须添加JSDoc注释 272 | 3. **测试覆盖** - 新增功能需添加相应测试 273 | 4. **文档更新** - 修改功能时同步更新文档 274 | 275 | ## 🌟 版本历史 276 | 277 | ### v1.2.3 (当前版本) 278 | 279 | - ✅ 增强AI回复稳定性 280 | - ✅ 优化控制面板UI 281 | - ✅ 修复已知浏览器兼容性问题 282 | - ✅ 提升防检测能力 283 | 284 | ### v1.1.0 285 | 286 | - ✅ 新增图片简历发送功能 287 | - ✅ 增强筛选条件配置 288 | - ✅ 改进日志系统 289 | - ✅ 优化性能表现 290 | 291 | ### v1.0.0 292 | 293 | - 🎉 初始版本发布 294 | - ✅ 基础自动化投递功能 295 | - ✅ AI智能回复系统 296 | - ✅ 可视化控制面板 297 | 298 | ## 🤝 参与贡献 299 | 300 | ### 贡献方式 301 | 302 | 1. **代码贡献** - 提交PR修复bug或添加新功能 303 | 2. **文档改进** - 完善使用文档和开发文档 304 | 3. **测试反馈** - 测试新功能并提交体验报告 305 | 4. **问题反馈** - 提交Issue报告bug或建议 306 | 307 | ### 开发团队 308 | 309 | - **Yangshengzhou** - 项目创始人和主要维护者 310 | - 欢迎更多开发者加入贡献! 311 | 312 | ### 贡献者名单 313 | 314 | [![Contributors](https://contrib.rocks/image?repo=YangShengzhou03/Jobs_helper)](https://github.com/YangShengzhou03/Jobs_helper/graphs/contributors) 315 | 316 | ## 📄 开源协议 317 | 318 | 本项目采用 **AGPL-3.0** 开源协议发布。 319 | 320 | ### 允许的行为 321 | 322 | - ✅ 自由使用和分发软件 323 | - ✅ 学习和研究源代码 324 | - ✅ 提交改进和修复 325 | - ✅ 在遵守协议的前提下进行商业使用 326 | 327 | ### 必须遵守的规则 328 | 329 | - 📛 修改版本必须开源并保留版权声明 330 | - 📛 分发时必须包含原始许可证 331 | - 📛 不得去除作者信息和变更说明 332 | - 📛 基于本项目的衍生作品必须使用相同协议 333 | 334 | 完整协议内容请参阅: [AGPL-3.0协议全文](https://www.gnu.org/licenses/agpl-3.0.html) 335 | 336 | ## 🐛 问题反馈 337 | 338 | ### 常见问题 339 | 340 | 1. **脚本不生效** 341 | - 检查Tampermonkey是否启用 342 | - 刷新BOSS直聘页面 343 | - 检查浏览器控制台错误信息 344 | 345 | 2. **AI回复失败** 346 | - 检查API密钥配置 347 | - 确认网络连接正常 348 | - 查看每日使用限额 349 | 350 | 3. **页面识别错误** 351 | - BOSS直聘页面结构更新 352 | - 等待脚本版本更新 353 | 354 | ### 提交Issue 355 | 356 | 请通过以下方式反馈问题: 357 | 358 | 1. **GitHub Issues**: [提交新Issue](https://github.com/YangShengzhou03/Jobs_helper/issues/new) 359 | 2. **问题模板**: 使用提供的issue模板 360 | 3. **必要信息**: 包括浏览器版本、错误日志、复现步骤 361 | 362 | ## 📞 支持与联系 363 | 364 | ### 官方渠道 365 | 366 | - **项目主页**: https://github.com/YangShengzhou03/Jobs_helper 367 | - **文档网站**: https://yangshengzhou.gitbook.io/jobs_helper 368 | - **问题反馈**: https://github.com/YangShengzhou03/Jobs_helper/issues 369 | 370 | ### 社区交流 371 | 372 | - **QQ群**: [点击加入](https://jq.qq.com/?_wv=1027&k=5F5J5z5x) (群号: 1021471813) 373 | - **微信公众号**: BOSS海投助手 374 | - **开发者邮箱**: 3555844679@qq.com 375 | 376 | ### 商务合作 377 | 378 | 如有商务合作需求,请邮件联系并注明"海投助手合作"。 379 | 380 | ## 💖 致谢 381 | 382 | 感谢所有为本项目做出贡献的开发者、测试者和用户! 383 | 384 | 特别感谢: 385 | - Tampermonkey团队提供的优秀脚本平台 386 | - 讯飞星火提供的AI能力支持 387 | - 所有提交反馈和改进建议的用户 388 | - 开源社区的持续支持和鼓励 389 | 390 | --- 391 | 392 | > *最后更新: 2025年10月* 393 | > *由 Yangshengzhou 开发和维护* 394 | > *你从不是孤身一人,我们与你共御就业寒冬,愿你能找到心仪的工作。* 💼✨ 395 | -------------------------------------------------------------------------------- /README.en.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | # 🚀 BOSS Job Helper (BOSS Helper) 🌟 4 | 5 | [![AGPL-3.0 License](https://img.shields.io/badge/License-AGPL_v3-blue.svg?style=for-the-badge&logo=gnu)](https://www.gnu.org/licenses/agpl-3.0) 6 | [![GitHub Stars](https://img.shields.io/github/stars/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=github)](https://github.com/YangShengzhou03/Jobs_helper) 7 | [![GitHub Forks](https://img.shields.io/github/forks/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=github)](https://github.com/YangShengzhou03/Jobs_helper) 8 | [![GitHub Issues](https://img.shields.io/github/issues/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=github)](https://github.com/YangShengzhou03/Jobs_helper/issues) 9 | [![GitHub Pull Requests](https://img.shields.io/github/issues-pr/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=github)](https://github.com/YangShengzhou03/Jobs_helper/pulls) 10 | [![Last Commit](https://img.shields.io/github/last-commit/YangShengzhou03/Jobs_helper?style=for-the-badge&logo=git)](https://github.com/YangShengzhou03/Jobs_helper) 11 | 12 | [![JavaScript](https://img.shields.io/badge/JavaScript-ES6+-yellow?style=flat-square&logo=javascript)](https://developer.mozilla.org/en-US/docs/Web/JavaScript) 13 | [![Tampermonkey](https://img.shields.io/badge/Tampermonkey-8.15+-green?style=flat-square&logo=tampermonkey)](https://www.tampermonkey.net/) 14 | [![Chrome](https://img.shields.io/badge/Chrome-88+-blue?style=flat-square&logo=google-chrome)](https://www.google.com/chrome/) 15 | [![Firefox](https://img.shields.io/badge/Firefox-85+-orange?style=flat-square&logo=firefox-browser)](https://www.mozilla.org/firefox/) 16 | 17 |
18 | 19 | ## 📖 Project Overview 20 | 21 | **BOSS Job Helper** is a browser userscript tool designed for job seekers to improve resume delivery efficiency and communication response speed on the [BOSS Zhipin platform](https://www.zhipin.com/). Through automated operations and AI-assisted replies, it helps users quickly filter suitable positions and complete resume delivery and message reply operations. 22 | 23 | ### 🎯 Core Features 24 | 25 | - 🤖 **Fully Automatic Batch Delivery** - Intelligently filters and automatically delivers all qualified positions 26 | - 🎯 **Multi-dimensional Precise Filtering** - Supports multiple condition filters including keywords, location, salary range 27 | - 💬 **AI Intelligent Replies** - Generates natural, professional HR message replies based on large language models 28 | - 🎨 **Modern Control Panel** - Visual operation interface with real-time task status monitoring 29 | - 🔄 **Anti-duplication Mechanism** - Automatically identifies already delivered positions to avoid repeated operations 30 | - 📊 **Detailed Log System** - Complete recording of all operations for easy debugging and analysis 31 | 32 | ## 🛠️ Technical Architecture 33 | 34 | ### System Architecture 35 | 36 | ``` 37 | BOSS Job Helper Architecture 38 | ├── 📦 Core Module (Core) 39 | │ ├── Automated Delivery Engine 40 | │ ├── Page Parser 41 | │ ├── AI Reply Processor 42 | │ └── State Management Machine 43 | ├── 🎨 UI Module (UI) 44 | │ ├── Control Panel System 45 | │ ├── Theme Management System 46 | │ └── Interactive Feedback Components 47 | ├── 💾 Data Module (State) 48 | │ ├── Local Storage Management 49 | │ ├── Session State Maintenance 50 | │ └── Configuration Persistence 51 | ├── 🔧 Utility Module (Utils) 52 | │ ├── DOM Operation Tools 53 | │ ├── Asynchronous Processing Tools 54 | │ └── Error Handling System 55 | └── ⚙️ Configuration Module (Config) 56 | ├── Runtime Configuration 57 | ├── Selector Configuration 58 | └── Constant Definitions 59 | ``` 60 | 61 | ### Technology Stack 62 | 63 | | Technology Area | Specific Technology | Version Requirements | 64 | |----------------|---------------------|---------------------| 65 | | **Core Language** | JavaScript (ES6+) | ES2015+ | 66 | | **Script Engine** | Tampermonkey / ScriptCat | 8.15+ | 67 | | **Browser Support** | Chrome, Firefox, Edge, Safari | Latest Version | 68 | | **AI Integration** | iFlytek Spark API / OpenAI API | - | 69 | | **Data Storage** | localStorage, IndexedDB | - | 70 | | **Build Tools** | Native JS, No Dependencies | - | 71 | 72 | ## 📦 Installation Guide 73 | 74 | ### Prerequisites 75 | 76 | 1. **Browser Extension** - Install one of the following script managers: 77 | - [Tampermonkey](https://www.tampermonkey.net/) (Recommended) 78 | - [ScriptCat](https://scriptcat.org/) 79 | 80 | 2. **Browser Version** - Supports modern browsers: 81 | - Chrome 88+ 82 | - Firefox 85+ 83 | - Edge 88+ 84 | - Safari 14+ 85 | 86 | ### Installation Steps 87 | 88 | #### Method 1: One-click Installation (Recommended) 89 | 90 | Click the link to install: [![Install Script](https://img.shields.io/badge/Install-Script-green?style=for-the-badge)](https://github.com/YangShengzhou03/Jobs_helper/raw/main/Boss_helper.js) 91 | 92 | #### Method 2: Manual Installation 93 | 94 | 1. Visit the project GitHub page: https://github.com/YangShengzhou03/Jobs_helper 95 | 2. Download the `Boss_helper.js` file 96 | 3. Click "New Script" in your script manager 97 | 4. Paste the file content and save 98 | 5. Refresh the BOSS Zhipin page to start using 99 | 100 | ## 🚀 Quick Start 101 | 102 | ### 1. Login to BOSS Zhipin 103 | 104 | Make sure you are logged into your BOSS Zhipin account 105 | 106 | ### 2. Access Supported Pages 107 | 108 | - **Job List Page**: https://www.zhipin.com/web/geek/jobs 109 | - **Chat Conversation Page**: https://www.zhipin.com/web/geek/chat 110 | 111 | ### 3. Configure Filter Conditions 112 | 113 | Set up in the control panel: 114 | - ✅ Job Keywords (e.g.: Frontend, Java, Python) 115 | - ✅ Work Locations (e.g.: Beijing, Shanghai, Shenzhen) 116 | - ✅ Salary Range Filter 117 | - ✅ Company Type Filter 118 | 119 | ### 4. Start Automation 120 | 121 | Click the "Start Delivery" button, the system will automatically: 122 | 1. Scan and filter qualified positions 123 | 2. Automatically enter each job detail page 124 | 3. Click the "立即沟通" (Contact Now) button 125 | 4. Send preset self-introduction messages 126 | 5. Record all operation logs 127 | 128 | ## 🎯 Feature Details 129 | 130 | ### 🤖 Automated Delivery System 131 | 132 | | Feature Module | Description | Technical Implementation | 133 | |----------------|-------------|--------------------------| 134 | | **Position Scanning** | Automatically scrolls to load all job lists | `MutationObserver` + Smart Scroll Detection | 135 | | **Condition Filtering** | Multi-dimensional precise matching of target positions | Regex Matching + Semantic Analysis | 136 | | **Auto Communication** | Simulates clicking the contact button | DOM Event Simulation + Async Waiting | 137 | | **Anti-duplication** | Identifies already processed HR and positions | localStorage + Hash Identification | 138 | 139 | ### 💬 AI Intelligent Reply System 140 | 141 | ```javascript 142 | // AI reply processing flow 143 | async function handleAIReply(hrMessage) { 144 | // 1. Message preprocessing 145 | const cleanedMessage = preprocessMessage(hrMessage); 146 | 147 | // 2. Intent recognition 148 | const intent = await detectIntent(cleanedMessage); 149 | 150 | // 3. Generate reply 151 | const reply = await generateReply(intent, cleanedMessage); 152 | 153 | // 4. Send reply 154 | await sendChatMessage(reply); 155 | } 156 | ``` 157 | 158 | ### 🎨 Control Panel Features 159 | 160 | - **Real-time Status Monitoring** - Displays current processing progress and statistics 161 | - **Dynamic Configuration Adjustment** - Real-time modification of filter conditions and operation parameters 162 | - **Theme Switching** - Supports light/dark theme modes 163 | - **Log Viewer** - Real-time display of operation logs and error information 164 | - **Performance Monitoring** - Shows memory usage and running time statistics 165 | 166 | ## ⚙️ Configuration Guide 167 | 168 | ### Basic Configuration 169 | 170 | ```javascript 171 | // config.js - Main configuration items 172 | const CONFIG = { 173 | BASIC_INTERVAL: 1000, // Basic operation interval (ms) 174 | OPERATION_INTERVAL: 800, // Specific operation interval (ms) 175 | MAX_REPLIES_FREE: 5, // Free version AI reply count 176 | MAX_REPLIES_PREMIUM: 10, // Premium version AI reply count 177 | DEFAULT_AI_ROLE: 'Job Seeker Role Setting', // AI default persona 178 | }; 179 | ``` 180 | 181 | ### AI Configuration 182 | 183 | Configure AI services in script settings: 184 | - iFlytek Spark API Key 185 | - OpenAI API Key (Optional) 186 | - Custom reply templates 187 | - Role setting configuration 188 | 189 | ### Filter Condition Configuration 190 | 191 | Supports multiple filter condition combinations: 192 | - Include/Exclude keywords 193 | - Geographical location range 194 | - Salary level range 195 | - Company size filtering 196 | - Industry type filtering 197 | 198 | ## 📊 Performance Metrics 199 | 200 | ### Processing Efficiency 201 | 202 | | Metric | Value | Description | 203 | |--------|-------|-------------| 204 | | Average Processing Speed | 2-3 seconds/position | From scanning to completion 205 | | Maximum Concurrency | 1 per tab | Single tab processing 206 | | Daily Processing Limit | 50 positions | Anti-abuse mechanism 207 | | Memory Usage | <10MB | Lightweight design 208 | 209 | ### Success Rate Statistics 210 | 211 | | Operation Type | Success Rate | Notes | 212 | |---------------|-------------|-------| 213 | | Position Scanning | 99.8% | Rare page structure changes | 214 | | Auto Communication | 98.5% | Depends on page loading speed | 215 | | AI Replies | 95.2% | Subject to network and API limitations | 216 | | Resume Sending | 97.3% | Requires HR to reply first | 217 | 218 | ## 🔧 Development Guide 219 | 220 | ### Project Structure 221 | 222 | ``` 223 | jobs-helper/ 224 | ├── 📄 Boss_helper.js # Main entry file 225 | ├── 📄 config.js # Configuration constants 226 | ├── 📄 core.js # Core business logic 227 | ├── 📄 ui.js # User interface components 228 | ├── 📄 state.js # State management 229 | ├── 📄 utils.js # Utility functions 230 | ├── 📄 letter.js # Guide messages 231 | ├── 📄 guide.js # User guide 232 | ├── 📄 settings.js # Settings panel 233 | └── 📄 README.md # Project documentation 234 | ``` 235 | 236 | ### Development Environment Setup 237 | 238 | ```bash 239 | # 1. Clone the project 240 | git clone https://github.com/YangShengzhou03/Jobs_helper.git 241 | 242 | # 2. Install dependencies (No build required, use directly) 243 | # This is a pure frontend project with no build dependencies 244 | 245 | # 3. Development debugging 246 | # Use browser developer tools for debugging 247 | # Recommended to use Tampermonkey's debug mode 248 | ``` 249 | 250 | ### Code Contribution 251 | 252 | Welcome to submit Pull Requests! Please follow these guidelines: 253 | 254 | 1. **Code Style** - Follow ES6+ syntax standards 255 | 2. **Comment Requirements** - Important functions must have JSDoc comments 256 | 3. **Test Coverage** - New features need corresponding tests 257 | 4. **Documentation Updates** - Update documentation when modifying features 258 | 259 | ## 🌟 Version History 260 | 261 | ### v1.2.3 (Current Version) 262 | 263 | - ✅ Enhanced AI reply stability 264 | - ✅ Optimized control panel UI 265 | - ✅ Fixed known browser compatibility issues 266 | - ✅ Improved anti-detection capability 267 | 268 | ### v1.1.0 269 | 270 | - ✅ Added image resume sending function 271 | - ✅ Enhanced filter condition configuration 272 | - ✅ Improved log system 273 | - ✅ Optimized performance 274 | 275 | ### v1.0.0 276 | 277 | - 🎉 Initial version release 278 | - ✅ Basic automated delivery function 279 | - ✅ AI intelligent reply system 280 | - ✅ Visual control panel 281 | 282 | ## 🤝 Contributing 283 | 284 | ### Contribution Methods 285 | 286 | 1. **Code Contribution** - Submit PRs to fix bugs or add new features 287 | 2. **Documentation Improvement** - Improve usage and development documentation 288 | 3. **Testing Feedback** - Test new features and submit experience reports 289 | 4. **Issue Reporting** - Submit issues to report bugs or suggestions 290 | 291 | ### Development Team 292 | 293 | - **Yangshengzhou** - Project creator and main maintainer 294 | - Welcome more developers to join and contribute! 295 | 296 | ### Contributors List 297 | 298 | [![Contributors](https://contrib.rocks/image?repo=YangShengzhou03/Jobs_helper)](https://github.com/YangShengzhou03/Jobs_helper/graphs/contributors) 299 | 300 | ## 📄 Open Source License 301 | 302 | This project is released under the **AGPL-3.0** open source license. 303 | 304 | ### Permitted Actions 305 | 306 | - ✅ Free use and distribution of software 307 | - ✅ Learning and researching source code 308 | - ✅ Submitting improvements and fixes 309 | - ✅ Commercial use under compliance with the license 310 | 311 | ### Required Compliance Rules 312 | 313 | - 📛 Modified versions must be open source and retain copyright notices 314 | - 📛 Distribution must include the original license 315 | - 📛 Cannot remove author information and change descriptions 316 | - 📛 Derivative works based on this project must use the same license 317 | 318 | Full license content: [AGPL-3.0 License Full Text](https://www.gnu.org/licenses/agpl-3.0.html) 319 | 320 | ## 🐛 Issue Reporting 321 | 322 | ### Common Issues 323 | 324 | 1. **Script Not Working** 325 | - Check if Tampermonkey is enabled 326 | - Refresh BOSS Zhipin page 327 | - Check browser console error messages 328 | 329 | 2. **AI Reply Failure** 330 | - Check API key configuration 331 | - Confirm network connection is normal 332 | - Check daily usage limits 333 | 334 | 3. **Page Recognition Error** 335 | - BOSS Zhipin page structure updated 336 | - Wait for script version update 337 | 338 | ### Submit Issue 339 | 340 | Please report issues through: 341 | 342 | 1. **GitHub Issues**: [Submit New Issue](https://github.com/YangShengzhou03/Jobs_helper/issues/new) 343 | 2. **Issue Template**: Use the provided issue template 344 | 3. **Required Information**: Include browser version, error logs, reproduction steps 345 | 346 | ## 📞 Support & Contact 347 | 348 | ### Official Channels 349 | 350 | - **Project Homepage**: https://github.com/YangShengzhou03/Jobs_helper 351 | - **Documentation Site**: https://yangshengzhou.gitbook.io/jobs_helper 352 | - **Issue Reporting**: https://github.com/YangShengzhou03/Jobs_helper/issues 353 | 354 | ### Community Communication 355 | 356 | - **QQ Group**: [Join Here](https://jq.qq.com/?_wv=1027&k=5F5J5z5x) (Group ID: 1021471813) 357 | - **WeChat Official Account**: BOSS海投助手 358 | - **Developer Email**: 3555844679@qq.com 359 | 360 | ### Business Cooperation 361 | 362 | For business cooperation needs, please email and indicate "BOSS Helper Cooperation". 363 | 364 | ## 💖 Acknowledgments 365 | 366 | Thanks to all developers, testers, and users who have contributed to this project! 367 | 368 | Special thanks to: 369 | - Tampermonkey team for the excellent script platform 370 | - iFlytek Spark for AI capability support 371 | - All users who submitted feedback and improvement suggestions 372 | - Open source community for continuous support and encouragement 373 | 374 | --- 375 | 376 |
377 | 378 | **🌟 If this project helps you, please give it a Star to show your support!** 379 | 380 | [![Star History Chart](https://api.star-history.com/svg?repos=YangShengzhou03/Jobs_helper&type=Date)](https://star-history.com/#YangShengzhou03/Jobs_helper&Date) 381 | 382 |
383 | 384 | --- 385 | 386 | *Last Updated: December 2024* 387 | *Developed and maintained by Yangshengzhou* 388 | *Wishing every job seeker finds their ideal job!* 💼✨ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . 662 | -------------------------------------------------------------------------------- /Boss_helper.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name BOSS海投助手 3 | // @namespace https://github.com/yangshengzhou03 4 | // @version 1.2.3.8 5 | // @description 求职工具!Yangshengzhou开发用于提高BOSS直聘投递效率,批量沟通,高效求职 6 | // @author Yangshengzhou 7 | // @match https://www.zhipin.com/web/* 8 | // @grant GM_xmlhttpRequest 9 | // @run-at document-idle 10 | // @supportURL https://github.com/yangshengzhou03 11 | // @homepageURL https://gitee.com/yangshengzhou 12 | // @license AGPL-3.0-or-later 13 | // @icon https://static.zhipin.com/favicon.ico 14 | // @connect zhipin.com 15 | // @connect spark-api-open.xf-yun.com 16 | // @noframes 17 | // @require https://cdnjs.cloudflare.com/ajax/libs/crypto-js/4.1.1/crypto-js.min.js 18 | // ==/UserScript== 19 | 20 | (function () { 21 | "use strict"; 22 | 23 | const CONFIG = { 24 | BASIC_INTERVAL: 1000, 25 | OPERATION_INTERVAL: 1200, 26 | 27 | DELAYS: { 28 | SHORT: 30, 29 | MEDIUM_SHORT: 200, 30 | DICT_LIST_LOAD: 300, 31 | RESUME_LIST_LOAD: 500, 32 | POSITION_DETAIL_LOAD: 800, 33 | LONG: 1000, 34 | }, 35 | COLORS: { 36 | PRIMARY: "#2196f3", 37 | SECONDARY: "#ff5722", 38 | }, 39 | MINI_ICON_SIZE: 40, 40 | SELECTORS: { 41 | JOB_LIST: { 42 | JOB_CARD: "li.job-card-box", 43 | CHAT_BTN: "a.op-btn-chat", 44 | }, 45 | CHAT: { 46 | CHAT_INPUT: "#chat-input", 47 | SEND_BUTTON: ".btn-send", 48 | FRIEND_MESSAGE: ".item-friend .text span", 49 | COMMON_PHRASE_BTN: ".btn-dict", 50 | RESUME_BTN: '.toolbar-btn:contains("发简历")', 51 | CONFIRM_SEND: "span.btn-sure-v2", 52 | IMAGE_SEND_BTN: 53 | '.toolbar-btn-content.icon.btn-sendimg input[type="file"]', 54 | }, 55 | }, 56 | AI: { 57 | MAX_REPLIES_FREE: 5, 58 | MAX_REPLIES_PREMIUM: 10, 59 | DEFAULT_ROLE: 60 | "你是求职的应届生正在面对HR,回复需满足:20字内,编造专业对口/实习经验/证书任一岗位优势;被问个人信息或岗位条件,直接配合提供合理数据;全程积极真诚无拒绝言辞。", 61 | }, 62 | MESSAGES: { 63 | JOB_MATCHED: "找到匹配岗位: ", 64 | JOB_NOT_FOUND: "没有找到符合条件的岗位", 65 | START_PROCESSING: "开始自动处理...", 66 | STOP_PROCESSING: "已停止自动处理", 67 | RESUME_SENT: "简历已发送", 68 | AI_REPLYING: "AI 正在回复...", 69 | MAX_REPLIES_REACHED: "今日 AI 回复次数已达上限", 70 | }, 71 | STORAGE_KEYS: { 72 | PROCESSED_HRS: "processedHRs", 73 | SENT_GREETINGS_HRS: "sentGreetingsHRs", 74 | SENT_RESUME_HRS: "sentResumeHRs", 75 | SENT_IMAGE_RESUME_HRS: "sentImageResumeHRs", 76 | AI_REPLY_COUNT: "aiReplyCount", 77 | LAST_AI_DATE: "lastAiDate", 78 | AI_ROLE: "aiRole", 79 | }, 80 | STORAGE_LIMITS: { 81 | PROCESSED_HRS: 500, 82 | SENT_GREETINGS_HRS: 500, 83 | SENT_RESUME_HRS: 300, 84 | SENT_IMAGE_RESUME_HRS: 300, 85 | }, 86 | }; 87 | 88 | const state = { 89 | isRunning: false, 90 | currentIndex: 0, 91 | 92 | includeKeywords: [], 93 | locationKeywords: [], 94 | 95 | jobList: [], 96 | 97 | ui: { 98 | isMinimized: false, 99 | theme: localStorage.getItem("theme") || "light", 100 | showWelcomeMessage: JSON.parse( 101 | localStorage.getItem("showWelcomeMessage") || "true" 102 | ), 103 | }, 104 | 105 | hrInteractions: { 106 | processedHRs: new Set( 107 | JSON.parse(localStorage.getItem("processedHRs") || "[]") 108 | ), 109 | sentGreetingsHRs: new Set( 110 | JSON.parse(localStorage.getItem("sentGreetingsHRs") || "[]") 111 | ), 112 | sentResumeHRs: new Set( 113 | JSON.parse(localStorage.getItem("sentResumeHRs") || "[]") 114 | ), 115 | sentImageResumeHRs: new Set( 116 | JSON.parse(localStorage.getItem("sentImageResumeHRs") || "[]") 117 | ), 118 | }, 119 | 120 | ai: { 121 | replyCount: JSON.parse(localStorage.getItem("aiReplyCount") || "0"), 122 | lastAiDate: localStorage.getItem("lastAiDate") || "", 123 | useAiReply: true, 124 | }, 125 | 126 | operation: { 127 | lastMessageTime: 0, 128 | }, 129 | 130 | user: { 131 | isPremiumUser: localStorage.getItem("isPremiumUser") === "true", 132 | }, 133 | 134 | settings: { 135 | useAutoSendResume: JSON.parse( 136 | localStorage.getItem("useAutoSendResume") || "true" 137 | ), 138 | useAutoSendImageResume: JSON.parse( 139 | localStorage.getItem("useAutoSendImageResume") || "false" 140 | ), 141 | imageResumes: JSON.parse(localStorage.getItem("imageResumes") || "[]"), 142 | autoScrollSpeed: parseInt( 143 | localStorage.getItem("autoScrollSpeed") || "500" 144 | ), 145 | customPhrases: JSON.parse(localStorage.getItem("customPhrases") || "[]"), 146 | actionDelays: { 147 | click: parseInt(localStorage.getItem("clickDelay") || "130"), 148 | }, 149 | notifications: { 150 | enabled: JSON.parse( 151 | localStorage.getItem("notificationsEnabled") || "true" 152 | ), 153 | sound: JSON.parse(localStorage.getItem("notificationSound") || "true"), 154 | }, 155 | }, 156 | }; 157 | 158 | const elements = { 159 | panel: null, 160 | controlBtn: null, 161 | log: null, 162 | includeInput: null, 163 | locationInput: null, 164 | miniIcon: null, 165 | }; 166 | 167 | class StorageManager { 168 | static setItem(key, value) { 169 | try { 170 | localStorage.setItem( 171 | key, 172 | typeof value === "string" ? value : JSON.stringify(value) 173 | ); 174 | return true; 175 | } catch (error) { 176 | Core.log(`设置存储项 ${key} 失败: ${error.message}`); 177 | return false; 178 | } 179 | } 180 | 181 | static getItem(key, defaultValue = null) { 182 | try { 183 | const value = localStorage.getItem(key); 184 | return value !== null ? value : defaultValue; 185 | } catch (error) { 186 | Core.log(`获取存储项 ${key} 失败: ${error.message}`); 187 | return defaultValue; 188 | } 189 | } 190 | 191 | static addRecordWithLimit(storageKey, record, currentSet, limit) { 192 | try { 193 | if (currentSet.has(record)) { 194 | return; 195 | } 196 | 197 | let records = this.getParsedItem(storageKey, []); 198 | records = Array.isArray(records) ? records : []; 199 | 200 | if (records.length >= limit) { 201 | records.shift(); 202 | } 203 | 204 | records.push(record); 205 | currentSet.add(record); 206 | this.setItem(storageKey, records); 207 | 208 | console.log( 209 | `存储管理: 添加记录${ 210 | records.length >= limit ? "并删除最早记录" : "" 211 | },当前${storageKey}数量: ${records.length}/${limit}` 212 | ); 213 | } catch (error) { 214 | console.log(`存储管理出错: ${error.message}`); 215 | } 216 | } 217 | 218 | static getParsedItem(storageKey, defaultValue = []) { 219 | try { 220 | const data = this.getItem(storageKey); 221 | return data ? JSON.parse(data) : defaultValue; 222 | } catch (error) { 223 | Core.log(`解析存储记录出错: ${error.message}`); 224 | return defaultValue; 225 | } 226 | } 227 | 228 | static ensureStorageLimits() { 229 | const limitConfigs = [ 230 | { 231 | key: CONFIG.STORAGE_KEYS.PROCESSED_HRS, 232 | set: state.hrInteractions.processedHRs, 233 | limit: CONFIG.STORAGE_LIMITS.PROCESSED_HRS, 234 | }, 235 | { 236 | key: CONFIG.STORAGE_KEYS.SENT_GREETINGS_HRS, 237 | set: state.hrInteractions.sentGreetingsHRs, 238 | limit: CONFIG.STORAGE_LIMITS.SENT_GREETINGS_HRS, 239 | }, 240 | { 241 | key: CONFIG.STORAGE_KEYS.SENT_RESUME_HRS, 242 | set: state.hrInteractions.sentResumeHRs, 243 | limit: CONFIG.STORAGE_LIMITS.SENT_RESUME_HRS, 244 | }, 245 | { 246 | key: CONFIG.STORAGE_KEYS.SENT_IMAGE_RESUME_HRS, 247 | set: state.hrInteractions.sentImageResumeHRs, 248 | limit: CONFIG.STORAGE_LIMITS.SENT_IMAGE_RESUME_HRS, 249 | }, 250 | ]; 251 | 252 | limitConfigs.forEach(({ key, set, limit }) => { 253 | const records = this.getParsedItem(key, []); 254 | if (records.length > limit) { 255 | const trimmedRecords = records.slice(-limit); 256 | this.setItem(key, trimmedRecords); 257 | 258 | set.clear(); 259 | trimmedRecords.forEach((record) => set.add(record)); 260 | 261 | console.log( 262 | `存储管理: 清理${key}记录,从${records.length}减少到${trimmedRecords.length}` 263 | ); 264 | } 265 | }); 266 | } 267 | } 268 | 269 | class StatePersistence { 270 | static saveState() { 271 | try { 272 | const stateMap = { 273 | aiReplyCount: state.ai.replyCount, 274 | lastAiDate: state.ai.lastAiDate, 275 | 276 | showWelcomeMessage: state.ui.showWelcomeMessage, 277 | isPremiumUser: state.user.isPremiumUser, 278 | useAiReply: state.ai.useAiReply, 279 | useAutoSendResume: state.settings.useAutoSendResume, 280 | useAutoSendImageResume: state.settings.useAutoSendImageResume, 281 | imageResumeData: state.settings.imageResumeData, 282 | imageResumes: state.settings.imageResumes || [], 283 | autoScrollSpeed: state.settings.autoScrollSpeed, 284 | customPhrases: state.settings.customPhrases, 285 | theme: state.ui.theme, 286 | clickDelay: state.settings.actionDelays.click, 287 | notificationsEnabled: state.settings.notifications.enabled, 288 | notificationSound: state.settings.notifications.sound, 289 | includeKeywords: state.includeKeywords, 290 | locationKeywords: state.locationKeywords, 291 | }; 292 | 293 | Object.entries(stateMap).forEach(([key, value]) => { 294 | StorageManager.setItem(key, value); 295 | }); 296 | } catch (error) { 297 | Core.log(`保存状态失败: ${error.message}`); 298 | } 299 | } 300 | 301 | static loadState() { 302 | try { 303 | state.includeKeywords = StorageManager.getParsedItem( 304 | "includeKeywords", 305 | [] 306 | ); 307 | state.locationKeywords = 308 | StorageManager.getParsedItem("locationKeywords") || 309 | StorageManager.getParsedItem("excludeKeywords", []); 310 | 311 | const imageResumes = StorageManager.getParsedItem("imageResumes", []); 312 | if (Array.isArray(imageResumes)) 313 | state.settings.imageResumes = imageResumes; 314 | 315 | StorageManager.ensureStorageLimits(); 316 | } catch (error) { 317 | Core.log(`加载状态失败: ${error.message}`); 318 | } 319 | } 320 | } 321 | 322 | class HRInteractionManager { 323 | static async handleHRInteraction(hrKey) { 324 | const hasResponded = await this.hasHRResponded(); 325 | 326 | if (!state.hrInteractions.sentGreetingsHRs.has(hrKey)) { 327 | await this._handleFirstInteraction(hrKey); 328 | return; 329 | } 330 | 331 | if ( 332 | !state.hrInteractions.sentResumeHRs.has(hrKey) || 333 | !state.hrInteractions.sentImageResumeHRs.has(hrKey) 334 | ) { 335 | if (hasResponded) { 336 | await this._handleFollowUpResponse(hrKey); 337 | } 338 | return; 339 | } 340 | 341 | await Core.aiReply(); 342 | } 343 | 344 | static async _handleFirstInteraction(hrKey) { 345 | Core.log(`首次沟通: ${hrKey}`); 346 | const sentGreeting = await this.sendGreetings(); 347 | 348 | if (sentGreeting) { 349 | StorageManager.addRecordWithLimit( 350 | CONFIG.STORAGE_KEYS.SENT_GREETINGS_HRS, 351 | hrKey, 352 | state.hrInteractions.sentGreetingsHRs, 353 | CONFIG.STORAGE_LIMITS.SENT_GREETINGS_HRS 354 | ); 355 | 356 | await this._handleResumeSending(hrKey); 357 | } 358 | } 359 | 360 | static async _handleResumeSending(hrKey) { 361 | if ( 362 | state.settings.useAutoSendResume && 363 | !state.hrInteractions.sentResumeHRs.has(hrKey) 364 | ) { 365 | const sentResume = await this.sendResume(); 366 | if (sentResume) { 367 | StorageManager.addRecordWithLimit( 368 | CONFIG.STORAGE_KEYS.SENT_RESUME_HRS, 369 | hrKey, 370 | state.hrInteractions.sentResumeHRs, 371 | CONFIG.STORAGE_LIMITS.SENT_RESUME_HRS 372 | ); 373 | } 374 | } 375 | 376 | if ( 377 | state.settings.useAutoSendImageResume && 378 | !state.hrInteractions.sentImageResumeHRs.has(hrKey) 379 | ) { 380 | const sentImageResume = await this.sendImageResume(); 381 | if (sentImageResume) { 382 | StorageManager.addRecordWithLimit( 383 | CONFIG.STORAGE_KEYS.SENT_IMAGE_RESUME_HRS, 384 | hrKey, 385 | state.hrInteractions.sentImageResumeHRs, 386 | CONFIG.STORAGE_LIMITS.SENT_IMAGE_RESUME_HRS 387 | ); 388 | } 389 | } 390 | } 391 | 392 | static async _handleFollowUpResponse(hrKey) { 393 | const lastMessage = await Core.getLastFriendMessageText(); 394 | 395 | if ( 396 | lastMessage && 397 | (lastMessage.includes("简历") || lastMessage.includes("发送简历")) 398 | ) { 399 | Core.log(`HR提到"简历",发送简历: ${hrKey}`); 400 | 401 | if ( 402 | state.settings.useAutoSendImageResume && 403 | !state.hrInteractions.sentImageResumeHRs.has(hrKey) 404 | ) { 405 | const sentImageResume = await this.sendImageResume(); 406 | if (sentImageResume) { 407 | state.hrInteractions.sentImageResumeHRs.add(hrKey); 408 | StatePersistence.saveState(); 409 | Core.log(`已向 ${hrKey} 发送图片简历`); 410 | return; 411 | } 412 | } 413 | 414 | if (!state.hrInteractions.sentResumeHRs.has(hrKey)) { 415 | const sentResume = await this.sendResume(); 416 | if (sentResume) { 417 | state.hrInteractions.sentResumeHRs.add(hrKey); 418 | StatePersistence.saveState(); 419 | Core.log(`已向 ${hrKey} 发送简历`); 420 | } 421 | } 422 | } 423 | } 424 | 425 | static async hasHRResponded() { 426 | await Core.delay(state.settings.actionDelays.click); 427 | 428 | const chatContainer = document.querySelector(".chat-message .im-list"); 429 | if (!chatContainer) return false; 430 | 431 | const friendMessages = Array.from( 432 | chatContainer.querySelectorAll("li.message-item.item-friend") 433 | ); 434 | return friendMessages.length > 0; 435 | } 436 | 437 | static async sendGreetings() { 438 | try { 439 | const dictBtn = await Core.waitForElement(".btn-dict"); 440 | if (!dictBtn) { 441 | Core.log("未找到常用语(自我介绍)按钮"); 442 | return false; 443 | } 444 | await Core.simulateClick(dictBtn); 445 | await Core.smartDelay(state.settings.actionDelays.click, "click"); 446 | await Core.smartDelay(300, "dict_load"); 447 | 448 | const dictList = await Core.waitForElement('ul[data-v-f115c50c=""]'); 449 | if (!dictList) { 450 | Core.log("未找到常用语(自我介绍)"); 451 | return false; 452 | } 453 | 454 | const dictItems = dictList.querySelectorAll("li"); 455 | if (!dictItems || dictItems.length === 0) { 456 | Core.log("常用语列表(自我介绍)为空"); 457 | return false; 458 | } 459 | 460 | for (let i = 0; i < dictItems.length; i++) { 461 | const item = dictItems[i]; 462 | Core.log( 463 | `发送常用语(自我介绍):第${i + 1}条/共${dictItems.length}条` 464 | ); 465 | await Core.simulateClick(item); 466 | await Core.delay(state.settings.actionDelays.click); 467 | } 468 | 469 | return true; 470 | } catch (error) { 471 | Core.log(`发送常用语出错: ${error.message}`); 472 | return false; 473 | } 474 | } 475 | 476 | static _findMatchingResume(resumeItems, positionName) { 477 | try { 478 | const positionNameLower = positionName.toLowerCase(); 479 | const twoCharKeywords = Core.extractTwoCharKeywords(positionNameLower); 480 | 481 | for (const keyword of twoCharKeywords) { 482 | for (const item of resumeItems) { 483 | const resumeNameElement = item.querySelector(".resume-name"); 484 | if (!resumeNameElement) continue; 485 | 486 | const resumeName = resumeNameElement.textContent 487 | .trim() 488 | .toLowerCase(); 489 | 490 | if (resumeName.includes(keyword)) { 491 | const resumeNameText = resumeNameElement.textContent.trim(); 492 | Core.log(`智能匹配: "${resumeNameText}" 依据: "${keyword}"`); 493 | return item; 494 | } 495 | } 496 | } 497 | 498 | return null; 499 | } catch (error) { 500 | Core.log(`简历匹配出错: ${error.message}`); 501 | return null; 502 | } 503 | } 504 | 505 | static async sendResume() { 506 | try { 507 | const resumeBtn = await Core.waitForElement(() => { 508 | return [...document.querySelectorAll(".toolbar-btn")].find( 509 | (el) => el.textContent.trim() === "发简历" 510 | ); 511 | }); 512 | 513 | if (!resumeBtn) { 514 | Core.log("无法发送简历,未找到发简历按钮"); 515 | return false; 516 | } 517 | 518 | if (resumeBtn.classList.contains("unable")) { 519 | Core.log("对方未回复,您无权发送简历"); 520 | return false; 521 | } 522 | 523 | let positionName = ""; 524 | try { 525 | const positionNameElement = 526 | Core.getCachedElement(".position-name", true) || 527 | Core.getCachedElement(".job-name", true) || 528 | Core.getCachedElement( 529 | '[class*="position-content"] .left-content .position-name', 530 | true 531 | ); 532 | 533 | if (positionNameElement) { 534 | positionName = positionNameElement.textContent.trim(); 535 | } else { 536 | Core.log("未找到岗位名称元素"); 537 | } 538 | } catch (e) { 539 | Core.log(`获取岗位名称出错: ${e.message}`); 540 | } 541 | 542 | await Core.simulateClick(resumeBtn); 543 | await Core.smartDelay(state.settings.actionDelays.click, "click"); 544 | await Core.smartDelay(800, "resume_load"); 545 | 546 | const confirmDialog = document.querySelector( 547 | ".panel-resume.sentence-popover" 548 | ); 549 | if (confirmDialog) { 550 | Core.log("您只有一份附件简历"); 551 | 552 | const confirmBtn = confirmDialog.querySelector(".btn-sure-v2"); 553 | if (!confirmBtn) { 554 | Core.log("未找到确认按钮"); 555 | return false; 556 | } 557 | 558 | await Core.simulateClick(confirmBtn); 559 | return true; 560 | } 561 | 562 | const resumeList = await Core.waitForElement("ul.resume-list"); 563 | if (!resumeList) { 564 | Core.log("未找到简历列表"); 565 | return false; 566 | } 567 | 568 | const resumeItems = Array.from( 569 | resumeList.querySelectorAll("li.list-item") 570 | ); 571 | if (resumeItems.length === 0) { 572 | Core.log("未找到简历列表项"); 573 | return false; 574 | } 575 | 576 | let selectedResumeItem = null; 577 | if (positionName) { 578 | selectedResumeItem = this._findMatchingResume( 579 | resumeItems, 580 | positionName 581 | ); 582 | } 583 | 584 | if (!selectedResumeItem) { 585 | selectedResumeItem = resumeItems[0]; 586 | const resumeName = selectedResumeItem 587 | .querySelector(".resume-name") 588 | .textContent.trim(); 589 | Core.log('使用第一个简历: "' + resumeName + '"'); 590 | } 591 | 592 | await Core.simulateClick(selectedResumeItem); 593 | await Core.smartDelay(state.settings.actionDelays.click, "click"); 594 | await Core.smartDelay(500, "selection"); 595 | 596 | const sendBtn = await Core.waitForElement( 597 | "button.btn-v2.btn-sure-v2.btn-confirm" 598 | ); 599 | if (!sendBtn) { 600 | Core.log("未找到发送按钮"); 601 | return false; 602 | } 603 | 604 | if (sendBtn.disabled) { 605 | Core.log("发送按钮不可用,可能简历未正确选择"); 606 | return false; 607 | } 608 | 609 | await Core.simulateClick(sendBtn); 610 | return true; 611 | } catch (error) { 612 | Core.log(`发送简历出错: ${error.message}`); 613 | return false; 614 | } 615 | } 616 | 617 | static selectImageResume(positionName) { 618 | try { 619 | const positionNameLower = positionName.toLowerCase(); 620 | 621 | if (state.settings.imageResumes.length === 1) { 622 | return state.settings.imageResumes[0]; 623 | } 624 | 625 | const twoCharKeywords = Core.extractTwoCharKeywords(positionNameLower); 626 | 627 | for (const keyword of twoCharKeywords) { 628 | for (const resume of state.settings.imageResumes) { 629 | const resumeNameLower = resume.path.toLowerCase(); 630 | 631 | if (resumeNameLower.includes(keyword)) { 632 | Core.log(`智能匹配: "${resume.path}" 依据: "${keyword}"`); 633 | return resume; 634 | } 635 | } 636 | } 637 | 638 | return state.settings.imageResumes[0]; 639 | } catch (error) { 640 | Core.log(`选择图片简历出错: ${error.message}`); 641 | return state.settings.imageResumes[0] || null; 642 | } 643 | } 644 | 645 | static async sendImageResume() { 646 | try { 647 | if ( 648 | !state.settings.useAutoSendImageResume || 649 | !state.settings.imageResumes || 650 | state.settings.imageResumes.length === 0 651 | ) { 652 | return false; 653 | } 654 | 655 | let positionName = ""; 656 | try { 657 | const positionNameElement = 658 | Core.getCachedElement(".position-name", true) || 659 | Core.getCachedElement(".job-name", true) || 660 | Core.getCachedElement( 661 | '[class*="position-content"] .left-content .position-name', 662 | true 663 | ); 664 | 665 | if (positionNameElement) { 666 | positionName = positionNameElement.textContent.trim(); 667 | } else { 668 | Core.log("未找到岗位名称元素"); 669 | positionName = ""; 670 | } 671 | } catch (e) { 672 | Core.log(`获取岗位名称出错: ${e.message}`); 673 | positionName = ""; 674 | } 675 | 676 | const selectedResume = this.selectImageResume(positionName); 677 | 678 | if (!selectedResume || !selectedResume.data) { 679 | Core.log("没有可发送的图片简历数据"); 680 | return false; 681 | } 682 | 683 | const imageSendBtn = await Core.waitForElement( 684 | '.toolbar-btn-content.icon.btn-sendimg input[type="file"]' 685 | ); 686 | if (!imageSendBtn) { 687 | Core.log("未找到图片发送按钮"); 688 | return false; 689 | } 690 | 691 | const byteCharacters = atob(selectedResume.data.split(",")[1]); 692 | const byteNumbers = new Array(byteCharacters.length); 693 | for (let i = 0; i < byteCharacters.length; i++) { 694 | byteNumbers[i] = byteCharacters.charCodeAt(i); 695 | } 696 | const byteArray = new Uint8Array(byteNumbers); 697 | const blob = new Blob([byteArray], { type: "image/jpeg" }); 698 | 699 | const file = new File([blob], selectedResume.path, { 700 | type: "image/jpeg", 701 | lastModified: new Date().getTime(), 702 | }); 703 | 704 | const dataTransfer = new DataTransfer(); 705 | dataTransfer.items.add(file); 706 | 707 | imageSendBtn.files = dataTransfer.files; 708 | 709 | const event = new Event("change", { bubbles: true }); 710 | imageSendBtn.dispatchEvent(event); 711 | return true; 712 | } catch (error) { 713 | Core.log(`发送图片出错: ${error.message}`); 714 | return false; 715 | } 716 | } 717 | } 718 | 719 | const UI = { 720 | PAGE_TYPES: { 721 | JOB_LIST: "jobList", 722 | CHAT: "chat", 723 | }, 724 | 725 | currentPageType: null, 726 | 727 | init() { 728 | this.currentPageType = location.pathname.includes("/chat") 729 | ? this.PAGE_TYPES.CHAT 730 | : this.PAGE_TYPES.JOB_LIST; 731 | this._applyTheme(); 732 | this.createControlPanel(); 733 | this.createMiniIcon(); 734 | }, 735 | 736 | createControlPanel() { 737 | if (document.getElementById("boss-pro-panel")) { 738 | document.getElementById("boss-pro-panel").remove(); 739 | } 740 | 741 | elements.panel = this._createPanel(); 742 | 743 | const header = this._createHeader(); 744 | const controls = this._createPageControls(); 745 | elements.log = this._createLogger(); 746 | const footer = this._createFooter(); 747 | 748 | elements.panel.append(header, controls, elements.log, footer); 749 | document.body.appendChild(elements.panel); 750 | this._makeDraggable(elements.panel); 751 | }, 752 | 753 | _applyTheme() { 754 | CONFIG.COLORS = 755 | this.currentPageType === this.PAGE_TYPES.JOB_LIST 756 | ? this.THEMES.JOB_LIST 757 | : this.THEMES.CHAT; 758 | 759 | document.documentElement.style.setProperty( 760 | "--primary-color", 761 | CONFIG.COLORS.primary 762 | ); 763 | document.documentElement.style.setProperty( 764 | "--secondary-color", 765 | CONFIG.COLORS.secondary 766 | ); 767 | document.documentElement.style.setProperty( 768 | "--accent-color", 769 | CONFIG.COLORS.accent 770 | ); 771 | document.documentElement.style.setProperty( 772 | "--neutral-color", 773 | CONFIG.COLORS.neutral 774 | ); 775 | }, 776 | 777 | THEMES: { 778 | JOB_LIST: { 779 | primary: "#4285f4", 780 | secondary: "#f5f7fa", 781 | accent: "#e8f0fe", 782 | neutral: "#6b7280", 783 | }, 784 | CHAT: { 785 | primary: "#34a853", 786 | secondary: "#f0fdf4", 787 | accent: "#dcfce7", 788 | neutral: "#6b7280", 789 | }, 790 | }, 791 | 792 | _createPanel() { 793 | const panel = document.createElement("div"); 794 | panel.id = "boss-pro-panel"; 795 | panel.className = 796 | this.currentPageType === this.PAGE_TYPES.JOB_LIST 797 | ? "boss-joblist-panel" 798 | : "boss-chat-panel"; 799 | 800 | const baseStyles = ` 801 | position: fixed; 802 | top: 36px; 803 | right: 24px; 804 | width: clamp(300px, 80vw, 400px); 805 | border-radius: 16px; 806 | padding: 18px; 807 | font-family: 'Segoe UI', system-ui, sans-serif; 808 | z-index: 2147483647; 809 | display: flex; 810 | flex-direction: column; 811 | transition: all 0.3s ease; 812 | background: #ffffff; 813 | box-shadow: 0 10px 25px rgba(var(--primary-rgb), 0.15); 814 | border: 1px solid var(--accent-color); 815 | cursor: default; 816 | `; 817 | 818 | panel.style.cssText = baseStyles; 819 | 820 | const rgbColor = this._hexToRgb(CONFIG.COLORS.primary); 821 | document.documentElement.style.setProperty("--primary-rgb", rgbColor); 822 | 823 | return panel; 824 | }, 825 | 826 | _createHeader() { 827 | const header = document.createElement("div"); 828 | header.className = 829 | this.currentPageType === this.PAGE_TYPES.JOB_LIST 830 | ? "boss-header" 831 | : "boss-chat-header"; 832 | 833 | header.style.cssText = ` 834 | display: flex; 835 | justify-content: space-between; 836 | align-items: center; 837 | padding: 0 10px 15px; 838 | margin-bottom: 15px; 839 | border-bottom: 1px solid var(--accent-color); 840 | `; 841 | 842 | const title = this._createTitle(); 843 | 844 | const buttonContainer = document.createElement("div"); 845 | buttonContainer.style.cssText = ` 846 | display: flex; 847 | gap: 8px; 848 | `; 849 | 850 | const buttonTitles = 851 | this.currentPageType === this.PAGE_TYPES.JOB_LIST 852 | ? { clear: "清空日志", settings: "插件设置", close: "最小化海投面板" } 853 | : { 854 | clear: "清空聊天记录", 855 | settings: "AI人设设置", 856 | close: "最小化聊天面板", 857 | }; 858 | 859 | const clearLogBtn = this._createIconButton( 860 | "🗑", 861 | () => { 862 | elements.log.innerHTML = `
欢迎使用海投助手,愿您在求职路上一帆风顺!
`; 863 | }, 864 | buttonTitles.clear 865 | ); 866 | 867 | const settingsBtn = this._createIconButton( 868 | "⚙", 869 | () => { 870 | showSettingsDialog(); 871 | }, 872 | buttonTitles.settings 873 | ); 874 | 875 | const closeBtn = this._createIconButton( 876 | "✕", 877 | () => { 878 | state.isMinimized = true; 879 | elements.panel.style.transform = "translateY(160%)"; 880 | elements.miniIcon.style.display = "flex"; 881 | }, 882 | buttonTitles.close 883 | ); 884 | 885 | buttonContainer.append(clearLogBtn, settingsBtn, closeBtn); 886 | header.append(title, buttonContainer); 887 | return header; 888 | }, 889 | 890 | _createTitle() { 891 | const title = document.createElement("div"); 892 | title.style.display = "flex"; 893 | title.style.alignItems = "center"; 894 | title.style.gap = "10px"; 895 | 896 | const customSvg = ` 897 | 899 | 900 | 901 | `; 902 | 903 | const titleConfig = 904 | this.currentPageType === this.PAGE_TYPES.JOB_LIST 905 | ? { 906 | main: `BOSS海投助手`, 907 | sub: "高效求职 · 智能匹配", 908 | } 909 | : { 910 | main: `BOSS智能聊天`, 911 | sub: "智能对话 · 高效沟通", 912 | }; 913 | 914 | title.innerHTML = ` 915 |
927 | ${customSvg} 928 |
929 |
930 |

936 | ${titleConfig.main} 937 |

938 | 942 | ${titleConfig.sub} 943 | 944 |
945 | `; 946 | 947 | return title; 948 | }, 949 | 950 | _createPageControls() { 951 | if (this.currentPageType === this.PAGE_TYPES.JOB_LIST) { 952 | return this._createJobListControls(); 953 | } else { 954 | return this._createChatControls(); 955 | } 956 | }, 957 | 958 | _createJobListControls() { 959 | const container = document.createElement("div"); 960 | container.className = "boss-joblist-controls"; 961 | container.style.marginBottom = "15px"; 962 | container.style.padding = "0 10px"; 963 | 964 | const filterContainer = this._createFilterContainer(); 965 | 966 | elements.controlBtn = this._createTextButton( 967 | "启动海投", 968 | "var(--primary-color)", 969 | () => { 970 | toggleProcess(); 971 | } 972 | ); 973 | 974 | container.append(filterContainer, elements.controlBtn); 975 | return container; 976 | }, 977 | 978 | _createChatControls() { 979 | const container = document.createElement("div"); 980 | container.className = "boss-chat-controls"; 981 | container.style.cssText = ` 982 | background: var(--secondary-color); 983 | border-radius: 12px; 984 | padding: 15px; 985 | margin-left: 10px; 986 | margin-right: 10px; 987 | margin-bottom: 15px; 988 | `; 989 | 990 | const configRow = document.createElement("div"); 991 | configRow.style.cssText = ` 992 | display: flex; 993 | gap: 10px; 994 | margin-bottom: 15px; 995 | `; 996 | 997 | const communicationIncludeCol = this._createInputControl( 998 | "沟通岗位包含:", 999 | "communication-include", 1000 | "如:技术,产品,设计" 1001 | ); 1002 | 1003 | const communicationModeCol = this._createSelectControl( 1004 | "沟通模式:", 1005 | "communication-mode-selector", 1006 | [ 1007 | { value: "new-only", text: "仅新消息" }, 1008 | { value: "auto", text: "自动轮询" }, 1009 | ] 1010 | ); 1011 | 1012 | elements.communicationIncludeInput = 1013 | communicationIncludeCol.querySelector("input"); 1014 | elements.communicationModeSelector = 1015 | communicationModeCol.querySelector("select"); 1016 | configRow.append(communicationIncludeCol, communicationModeCol); 1017 | 1018 | elements.communicationModeSelector.addEventListener("change", (e) => { 1019 | settings.communicationMode = e.target.value; 1020 | saveSettings(); 1021 | }); 1022 | 1023 | elements.communicationIncludeInput.addEventListener("input", (e) => { 1024 | settings.communicationIncludeKeywords = e.target.value; 1025 | saveSettings(); 1026 | }); 1027 | 1028 | elements.controlBtn = this._createTextButton( 1029 | "开始智能聊天", 1030 | "var(--primary-color)", 1031 | () => { 1032 | toggleChatProcess(); 1033 | } 1034 | ); 1035 | 1036 | container.append(configRow, elements.controlBtn); 1037 | return container; 1038 | }, 1039 | 1040 | _createFilterContainer() { 1041 | const filterContainer = document.createElement("div"); 1042 | filterContainer.style.cssText = ` 1043 | background: var(--secondary-color); 1044 | border-radius: 12px; 1045 | padding: 15px; 1046 | margin-bottom: 15px; 1047 | `; 1048 | 1049 | const filterRow = document.createElement("div"); 1050 | filterRow.style.cssText = ` 1051 | display: flex; 1052 | gap: 10px; 1053 | margin-bottom: 12px; 1054 | `; 1055 | 1056 | const includeFilterCol = this._createInputControl( 1057 | "职位名包含:", 1058 | "include-filter", 1059 | "如:前端,开发" 1060 | ); 1061 | const locationFilterCol = this._createInputControl( 1062 | "工作地包含:", 1063 | "location-filter", 1064 | "如:杭州,滨江" 1065 | ); 1066 | 1067 | elements.includeInput = includeFilterCol.querySelector("input"); 1068 | elements.locationInput = locationFilterCol.querySelector("input"); 1069 | 1070 | filterRow.append(includeFilterCol, locationFilterCol); 1071 | 1072 | const joinGroupBtn = document.createElement("button"); 1073 | joinGroupBtn.className = "boss-advanced-filter-btn"; 1074 | joinGroupBtn.innerHTML = ' 海投服务群'; 1075 | joinGroupBtn.style.cssText = ` 1076 | width: 100%; 1077 | padding: 8px 10px; 1078 | background: white; 1079 | color: var(--primary-color); 1080 | border: 1px solid var(--primary-color); 1081 | border-radius: 8px; 1082 | cursor: pointer; 1083 | font-size: 14px; 1084 | text-align: center; 1085 | transition: all 0.2s ease; 1086 | display: flex; 1087 | justify-content: center; 1088 | align-items: center; 1089 | gap: 5px; 1090 | `; 1091 | 1092 | joinGroupBtn.addEventListener("click", () => { 1093 | window.open("https://qm.qq.com/q/ZNOz2ZZb6S", "_blank"); 1094 | }); 1095 | 1096 | joinGroupBtn.addEventListener("mouseenter", () => { 1097 | joinGroupBtn.style.backgroundColor = "var(--primary-color)"; 1098 | joinGroupBtn.style.color = "white"; 1099 | }); 1100 | 1101 | joinGroupBtn.addEventListener("mouseleave", () => { 1102 | joinGroupBtn.style.backgroundColor = "white"; 1103 | joinGroupBtn.style.color = "var(--primary-color)"; 1104 | }); 1105 | 1106 | filterContainer.append(filterRow, joinGroupBtn); 1107 | return filterContainer; 1108 | }, 1109 | 1110 | _createInputControl(labelText, id, placeholder) { 1111 | const controlCol = document.createElement("div"); 1112 | controlCol.style.cssText = "flex: 1;"; 1113 | 1114 | const label = document.createElement("label"); 1115 | label.textContent = labelText; 1116 | label.style.cssText = 1117 | "display:block; margin-bottom:5px; font-weight: 500; color: #333; font-size: 0.9rem;"; 1118 | 1119 | const input = document.createElement("input"); 1120 | input.id = id; 1121 | input.placeholder = placeholder; 1122 | input.className = "boss-filter-input"; 1123 | input.style.cssText = ` 1124 | width: 100%; 1125 | padding: 8px 10px; 1126 | border-radius: 8px; 1127 | border: 1px solid #d1d5db; 1128 | font-size: 14px; 1129 | box-shadow: 0 1px 2px rgba(0,0,0,0.05); 1130 | transition: all 0.2s ease; 1131 | `; 1132 | 1133 | controlCol.append(label, input); 1134 | return controlCol; 1135 | }, 1136 | 1137 | _createSelectControl(labelText, id, options) { 1138 | const controlCol = document.createElement("div"); 1139 | controlCol.style.cssText = "flex: 1;"; 1140 | 1141 | const label = document.createElement("label"); 1142 | label.textContent = labelText; 1143 | label.style.cssText = 1144 | "display:block; margin-bottom:5px; font-weight: 500; color: #333; font-size: 0.9rem;"; 1145 | 1146 | const select = document.createElement("select"); 1147 | select.id = id; 1148 | select.style.cssText = ` 1149 | width: 100%; 1150 | padding: 8px 10px; 1151 | border-radius: 8px; 1152 | border: 1px solid #d1d5db; 1153 | font-size: 14px; 1154 | background: white; 1155 | color: #333; 1156 | box-shadow: 0 1px 2px rgba(0,0,0,0.05); 1157 | transition: all 0.2s ease; 1158 | `; 1159 | 1160 | options.forEach((option) => { 1161 | const opt = document.createElement("option"); 1162 | opt.value = option.value; 1163 | opt.textContent = option.text; 1164 | select.appendChild(opt); 1165 | }); 1166 | 1167 | controlCol.append(label, select); 1168 | return controlCol; 1169 | }, 1170 | 1171 | _createLogger() { 1172 | const log = document.createElement("div"); 1173 | log.id = "pro-log"; 1174 | log.className = 1175 | this.currentPageType === this.PAGE_TYPES.JOB_LIST 1176 | ? "boss-joblist-log" 1177 | : "boss-chat-log"; 1178 | 1179 | const height = 1180 | this.currentPageType === this.PAGE_TYPES.JOB_LIST ? "180px" : "220px"; 1181 | 1182 | log.style.cssText = ` 1183 | height: ${height}; 1184 | overflow-y: auto; 1185 | background: var(--secondary-color); 1186 | border-radius: 12px; 1187 | padding: 12px; 1188 | font-size: 13px; 1189 | line-height: 1.5; 1190 | margin-bottom: 15px; 1191 | margin-left: 10px; 1192 | margin-right: 10px; 1193 | transition: all 0.3s ease; 1194 | user-select: text; 1195 | scrollbar-width: thin; 1196 | scrollbar-color: var(--primary-color) var(--secondary-color); 1197 | `; 1198 | 1199 | log.innerHTML += ` 1200 | 1213 | `; 1214 | 1215 | return log; 1216 | }, 1217 | 1218 | _createFooter() { 1219 | const footer = document.createElement("div"); 1220 | footer.className = 1221 | this.currentPageType === this.PAGE_TYPES.JOB_LIST 1222 | ? "boss-joblist-footer" 1223 | : "boss-chat-footer"; 1224 | 1225 | footer.style.cssText = ` 1226 | text-align: center; 1227 | font-size: 0.8em; 1228 | color: var(--neutral-color); 1229 | padding-top: 15px; 1230 | border-top: 1px solid var(--accent-color); 1231 | margin-top: auto; 1232 | padding: 15px; 1233 | `; 1234 | 1235 | const statsContainer = document.createElement("div"); 1236 | statsContainer.style.cssText = ` 1237 | display: flex; 1238 | justify-content: space-around; 1239 | margin-bottom: 15px; 1240 | `; 1241 | 1242 | footer.append( 1243 | statsContainer, 1244 | document.createTextNode("© 2025 Yangshengzhou · All Rights Reserved") 1245 | ); 1246 | return footer; 1247 | }, 1248 | 1249 | _createTextButton(text, bgColor, onClick) { 1250 | const btn = document.createElement("button"); 1251 | btn.className = "boss-btn"; 1252 | btn.textContent = text; 1253 | btn.style.cssText = ` 1254 | width: 100%; 1255 | padding: 10px 16px; 1256 | background: ${bgColor}; 1257 | color: #fff; 1258 | border: none; 1259 | border-radius: 10px; 1260 | cursor: pointer; 1261 | font-size: 15px; 1262 | font-weight: 500; 1263 | transition: all 0.3s ease; 1264 | display: flex; 1265 | justify-content: center; 1266 | align-items: center; 1267 | box-shadow: 0 4px 10px rgba(0,0,0,0.1); 1268 | transform: translateY(0px); 1269 | margin: 0 auto; 1270 | `; 1271 | 1272 | this._addButtonHoverEffects(btn); 1273 | btn.addEventListener("click", onClick); 1274 | 1275 | return btn; 1276 | }, 1277 | 1278 | _createIconButton(icon, onClick, title) { 1279 | const btn = document.createElement("button"); 1280 | btn.className = "boss-icon-btn"; 1281 | btn.innerHTML = icon; 1282 | btn.title = title; 1283 | btn.style.cssText = ` 1284 | width: 32px; 1285 | height: 32px; 1286 | border-radius: 50%; 1287 | border: none; 1288 | background: ${ 1289 | this.currentPageType === this.PAGE_TYPES.JOB_LIST 1290 | ? "var(--accent-color)" 1291 | : "var(--accent-color)" 1292 | }; 1293 | cursor: pointer; 1294 | font-size: 16px; 1295 | transition: all 0.2s ease; 1296 | display: flex; 1297 | justify-content: center; 1298 | align-items: center; 1299 | color: var(--primary-color); 1300 | `; 1301 | 1302 | btn.addEventListener("click", onClick); 1303 | btn.addEventListener("mouseenter", () => { 1304 | btn.style.backgroundColor = "var(--primary-color)"; 1305 | btn.style.color = "#fff"; 1306 | btn.style.transform = "scale(1.1)"; 1307 | }); 1308 | 1309 | btn.addEventListener("mouseleave", () => { 1310 | btn.style.backgroundColor = 1311 | this.currentPageType === this.PAGE_TYPES.JOB_LIST 1312 | ? "var(--accent-color)" 1313 | : "var(--accent-color)"; 1314 | btn.style.color = "var(--primary-color)"; 1315 | btn.style.transform = "scale(1)"; 1316 | }); 1317 | 1318 | return btn; 1319 | }, 1320 | 1321 | _addButtonHoverEffects(btn) { 1322 | btn.addEventListener("mouseenter", () => { 1323 | btn.style.transform = "translateY(-2px)"; 1324 | btn.style.boxShadow = `0 6px 15px rgba(var(--primary-rgb), 0.3)`; 1325 | }); 1326 | 1327 | btn.addEventListener("mouseleave", () => { 1328 | btn.style.transform = "translateY(0)"; 1329 | btn.style.boxShadow = "0 4px 10px rgba(0,0,0,0.1)"; 1330 | }); 1331 | }, 1332 | 1333 | async scrollUserList() { 1334 | const userList = document.querySelector( 1335 | 'div[data-v-35229736=""].user-list' 1336 | ); 1337 | if (!userList) return; 1338 | 1339 | const userItems = userList.querySelectorAll('li[role="listitem"]'); 1340 | let currentIndex = 0; 1341 | 1342 | for (let i = 0; i < userItems.length; i++) { 1343 | if (userItems[i].classList.contains("last-clicked")) { 1344 | currentIndex = i; 1345 | break; 1346 | } 1347 | } 1348 | 1349 | const nextIndex = currentIndex + 1; 1350 | if (nextIndex < userItems.length) { 1351 | const nextItem = userItems[nextIndex]; 1352 | nextItem.scrollIntoView({ 1353 | behavior: "smooth", 1354 | block: "center", 1355 | }); 1356 | 1357 | this.log(`自动滚动到第 ${nextIndex + 1} 个用户`); 1358 | } else { 1359 | this.log("已到达用户列表末尾"); 1360 | } 1361 | }, 1362 | 1363 | _makeDraggable(panel) { 1364 | const header = panel.querySelector(".boss-header, .boss-chat-header"); 1365 | 1366 | if (!header) return; 1367 | 1368 | header.style.cursor = "move"; 1369 | 1370 | let isDragging = false; 1371 | let startX = 0, 1372 | startY = 0; 1373 | let initialX = panel.offsetLeft, 1374 | initialY = panel.offsetTop; 1375 | 1376 | header.addEventListener("mousedown", (e) => { 1377 | isDragging = true; 1378 | startX = e.clientX; 1379 | startY = e.clientY; 1380 | initialX = panel.offsetLeft; 1381 | initialY = panel.offsetTop; 1382 | panel.style.transition = "none"; 1383 | panel.style.zIndex = "2147483647"; 1384 | }); 1385 | 1386 | document.addEventListener("mousemove", (e) => { 1387 | if (!isDragging) return; 1388 | 1389 | const dx = e.clientX - startX; 1390 | const dy = e.clientY - startY; 1391 | 1392 | panel.style.left = `${initialX + dx}px`; 1393 | panel.style.top = `${initialY + dy}px`; 1394 | panel.style.right = "auto"; 1395 | }); 1396 | 1397 | document.addEventListener("mouseup", () => { 1398 | if (isDragging) { 1399 | isDragging = false; 1400 | panel.style.transition = "all 0.3s ease"; 1401 | panel.style.zIndex = "2147483646"; 1402 | } 1403 | }); 1404 | }, 1405 | 1406 | createMiniIcon() { 1407 | elements.miniIcon = document.createElement("div"); 1408 | elements.miniIcon.style.cssText = ` 1409 | width: ${CONFIG.MINI_ICON_SIZE || 48}px; 1410 | height: ${CONFIG.MINI_ICON_SIZE || 48}px; 1411 | position: fixed; 1412 | bottom: 40px; 1413 | left: 40px; 1414 | background: var(--primary-color); 1415 | border-radius: 50%; 1416 | box-shadow: 0 6px 16px rgba(var(--primary-rgb), 0.4); 1417 | cursor: pointer; 1418 | display: none; 1419 | justify-content: center; 1420 | align-items: center; 1421 | color: #fff; 1422 | z-index: 2147483647; 1423 | transition: all 0.3s ease; 1424 | overflow: hidden; 1425 | 1426 | `; 1427 | 1428 | const customSvg = ` 1429 | 1431 | 1432 | 1433 | `; 1434 | 1435 | elements.miniIcon.innerHTML = customSvg; 1436 | 1437 | elements.miniIcon.addEventListener("mouseenter", () => { 1438 | elements.miniIcon.style.transform = "scale(1.1)"; 1439 | elements.miniIcon.style.boxShadow = `0 8px 20px rgba(var(--primary-rgb), 0.5)`; 1440 | }); 1441 | 1442 | elements.miniIcon.addEventListener("mouseleave", () => { 1443 | elements.miniIcon.style.transform = "scale(1)"; 1444 | elements.miniIcon.style.boxShadow = `0 6px 16px rgba(var(--primary-rgb), 0.4)`; 1445 | }); 1446 | 1447 | elements.miniIcon.addEventListener("click", () => { 1448 | state.isMinimized = false; 1449 | elements.panel.style.transform = "translateY(0)"; 1450 | elements.miniIcon.style.display = "none"; 1451 | }); 1452 | 1453 | document.body.appendChild(elements.miniIcon); 1454 | }, 1455 | 1456 | _hexToRgb(hex) { 1457 | hex = hex.replace("#", ""); 1458 | 1459 | const r = parseInt(hex.substring(0, 2), 16); 1460 | const g = parseInt(hex.substring(2, 4), 16); 1461 | const b = parseInt(hex.substring(4, 6), 16); 1462 | 1463 | return `${r}, ${g}, ${b}`; 1464 | }, 1465 | }; 1466 | 1467 | const settings = { 1468 | useAutoSendResume: JSON.parse( 1469 | localStorage.getItem("useAutoSendResume") || "true" 1470 | ), 1471 | autoScrollSpeed: parseInt(localStorage.getItem("autoScrollSpeed") || "500"), 1472 | customPhrases: JSON.parse(localStorage.getItem("customPhrases") || "[]"), 1473 | actionDelays: { 1474 | click: parseInt(localStorage.getItem("clickDelay") || "130"), 1475 | }, 1476 | notifications: { 1477 | enabled: JSON.parse( 1478 | localStorage.getItem("notificationsEnabled") || "true" 1479 | ), 1480 | sound: JSON.parse(localStorage.getItem("notificationSound") || "true"), 1481 | }, 1482 | ai: { 1483 | role: 1484 | localStorage.getItem("aiRole") || 1485 | "你是求职的应届生正在面对HR,回复需满足:20字内,编造专业对口/实习经验/证书任一岗位优势;被问个人信息或岗位条件,直接配合提供合理数据;全程积极真诚无拒绝言辞。", 1486 | }, 1487 | 1488 | useAutoSendImageResume: JSON.parse( 1489 | localStorage.getItem("useAutoSendImageResume") || "false" 1490 | ), 1491 | imageResumeData: localStorage.getItem("imageResumeData") || null, 1492 | 1493 | communicationMode: localStorage.getItem("communicationMode") || "new-only", 1494 | 1495 | recruiterActivityStatus: JSON.parse( 1496 | localStorage.getItem("recruiterActivityStatus") || '["不限"]' 1497 | ), 1498 | 1499 | intervals: { 1500 | basic: 1501 | parseInt(localStorage.getItem("basicInterval")) || 1502 | CONFIG.BASIC_INTERVAL, 1503 | operation: 1504 | parseInt(localStorage.getItem("operationInterval")) || 1505 | CONFIG.OPERATION_INTERVAL, 1506 | }, 1507 | }; 1508 | 1509 | function saveSettings() { 1510 | localStorage.setItem( 1511 | "useAutoSendResume", 1512 | settings.useAutoSendResume.toString() 1513 | ); 1514 | localStorage.setItem( 1515 | "autoScrollSpeed", 1516 | settings.autoScrollSpeed.toString() 1517 | ); 1518 | localStorage.setItem( 1519 | "customPhrases", 1520 | JSON.stringify(settings.customPhrases) 1521 | ); 1522 | localStorage.setItem("clickDelay", settings.actionDelays.click.toString()); 1523 | localStorage.setItem( 1524 | "notificationsEnabled", 1525 | settings.notifications.enabled.toString() 1526 | ); 1527 | localStorage.setItem( 1528 | "notificationSound", 1529 | settings.notifications.sound.toString() 1530 | ); 1531 | localStorage.setItem("aiRole", settings.ai.role); 1532 | 1533 | localStorage.setItem( 1534 | "useAutoSendImageResume", 1535 | settings.useAutoSendImageResume.toString() 1536 | ); 1537 | 1538 | if (settings.imageResumes) { 1539 | localStorage.setItem( 1540 | "imageResumes", 1541 | JSON.stringify(settings.imageResumes) 1542 | ); 1543 | } 1544 | 1545 | if (settings.imageResumeData) { 1546 | localStorage.setItem("imageResumeData", settings.imageResumeData); 1547 | } else { 1548 | localStorage.removeItem("imageResumeData"); 1549 | } 1550 | 1551 | localStorage.setItem( 1552 | "recruiterActivityStatus", 1553 | JSON.stringify(settings.recruiterActivityStatus) 1554 | ); 1555 | 1556 | localStorage.setItem("basicInterval", settings.intervals.basic.toString()); 1557 | localStorage.setItem( 1558 | "operationInterval", 1559 | settings.intervals.operation.toString() 1560 | ); 1561 | } 1562 | 1563 | function loadSettings() { 1564 | settings.useAutoSendResume = JSON.parse( 1565 | localStorage.getItem("useAutoSendResume") || "true" 1566 | ); 1567 | settings.autoScrollSpeed = parseInt( 1568 | localStorage.getItem("autoScrollSpeed") || "500" 1569 | ); 1570 | settings.customPhrases = JSON.parse( 1571 | localStorage.getItem("customPhrases") || "[]" 1572 | ); 1573 | settings.actionDelays.click = parseInt( 1574 | localStorage.getItem("clickDelay") || "130" 1575 | ); 1576 | settings.notifications.enabled = JSON.parse( 1577 | localStorage.getItem("notificationsEnabled") || "true" 1578 | ); 1579 | settings.notifications.sound = JSON.parse( 1580 | localStorage.getItem("notificationSound") || "true" 1581 | ); 1582 | settings.ai.role = 1583 | localStorage.getItem("aiRole") || 1584 | "你是求职的应届生正在面对HR,回复需满足:20字内,编造专业对口/实习经验/证书任一岗位优势;被问个人信息或岗位条件,直接配合提供合理数据;全程积极真诚无拒绝言辞。"; 1585 | 1586 | settings.useAutoSendImageResume = JSON.parse( 1587 | localStorage.getItem("useAutoSendImageResume") || "false" 1588 | ); 1589 | 1590 | try { 1591 | settings.imageResumes = 1592 | JSON.parse(localStorage.getItem("imageResumes")) || []; 1593 | } catch (e) { 1594 | settings.imageResumes = []; 1595 | } 1596 | 1597 | try { 1598 | settings.recruiterActivityStatus = JSON.parse( 1599 | localStorage.getItem("recruiterActivityStatus") 1600 | ) || ["不限"]; 1601 | } catch (e) { 1602 | settings.recruiterActivityStatus = ["不限"]; 1603 | } 1604 | 1605 | settings.imageResumeData = localStorage.getItem("imageResumeData") || null; 1606 | 1607 | settings.communicationMode = 1608 | localStorage.getItem("communicationMode") || "new-only"; 1609 | 1610 | settings.intervals = { 1611 | basic: 1612 | parseInt(localStorage.getItem("basicInterval")) || 1613 | CONFIG.BASIC_INTERVAL, 1614 | operation: 1615 | parseInt(localStorage.getItem("operationInterval")) || 1616 | CONFIG.OPERATION_INTERVAL, 1617 | }; 1618 | } 1619 | 1620 | function createSettingsDialog() { 1621 | const dialog = document.createElement("div"); 1622 | dialog.id = "boss-settings-dialog"; 1623 | dialog.style.cssText = ` 1624 | position: fixed; 1625 | top: 50%; 1626 | left: 50%; 1627 | transform: translate(-50%, -50%); 1628 | width: clamp(300px, 90vw, 550px); 1629 | height: 80vh; 1630 | background: #ffffff; 1631 | border-radius: 12px; 1632 | box-shadow: 0 10px 30px rgba(0,0,0,0.15); 1633 | z-index: 999999; 1634 | display: none; 1635 | flex-direction: column; 1636 | font-family: 'Segoe UI', sans-serif; 1637 | overflow: hidden; 1638 | transition: all 0.3s ease; 1639 | `; 1640 | 1641 | dialog.innerHTML += ` 1642 | 1710 | `; 1711 | 1712 | const dialogHeader = createDialogHeader("海投助手·BOSS设置"); 1713 | 1714 | const dialogContent = document.createElement("div"); 1715 | dialogContent.style.cssText = ` 1716 | padding: 18px; 1717 | flex: 1; 1718 | overflow-y: auto; 1719 | scrollbar-width: thin; 1720 | scrollbar-color: rgba(0, 123, 255, 0.5) rgba(0, 0, 0, 0.05); 1721 | `; 1722 | 1723 | dialogContent.innerHTML += ` 1724 | 1745 | `; 1746 | 1747 | const tabsContainer = document.createElement("div"); 1748 | tabsContainer.style.cssText = ` 1749 | display: flex; 1750 | border-bottom: 1px solid rgba(0, 123, 255, 0.2); 1751 | margin-bottom: 20px; 1752 | `; 1753 | 1754 | const aiTab = document.createElement("button"); 1755 | aiTab.textContent = "AI人设"; 1756 | aiTab.className = "settings-tab active"; 1757 | aiTab.style.cssText = ` 1758 | padding: 9px 15px; 1759 | background: rgba(0, 123, 255, 0.9); 1760 | color: white; 1761 | border: none; 1762 | border-radius: 8px 8px 0 0; 1763 | font-weight: 500; 1764 | cursor: pointer; 1765 | transition: all 0.2s ease; 1766 | margin-right: 5px; 1767 | `; 1768 | 1769 | const advancedTab = document.createElement("button"); 1770 | advancedTab.textContent = "高级设置"; 1771 | advancedTab.className = "settings-tab"; 1772 | advancedTab.style.cssText = ` 1773 | padding: 9px 15px; 1774 | background: rgba(0, 0, 0, 0.05); 1775 | color: #333; 1776 | border: none; 1777 | border-radius: 8px 8px 0 0; 1778 | font-weight: 500; 1779 | cursor: pointer; 1780 | transition: all 0.2s ease; 1781 | margin-right: 5px; 1782 | `; 1783 | 1784 | const intervalTab = document.createElement("button"); 1785 | intervalTab.textContent = "间隔设置"; 1786 | intervalTab.className = "settings-tab"; 1787 | intervalTab.style.cssText = ` 1788 | padding: 9px 15px; 1789 | background: rgba(0, 0, 0, 0.05); 1790 | color: #333; 1791 | border: none; 1792 | border-radius: 8px 8px 0 0; 1793 | font-weight: 500; 1794 | cursor: pointer; 1795 | transition: all 0.2s ease; 1796 | margin-right: 5px; 1797 | `; 1798 | 1799 | tabsContainer.append(aiTab, advancedTab, intervalTab); 1800 | 1801 | const aiSettingsPanel = document.createElement("div"); 1802 | aiSettingsPanel.id = "ai-settings-panel"; 1803 | 1804 | const roleSettingResult = createSettingItem( 1805 | "AI角色定位", 1806 | "定义AI在对话中的角色和语气特点", 1807 | () => document.getElementById("ai-role-input") 1808 | ); 1809 | 1810 | const roleSetting = roleSettingResult.settingItem; 1811 | 1812 | const roleInput = document.createElement("textarea"); 1813 | roleInput.id = "ai-role-input"; 1814 | roleInput.rows = 5; 1815 | roleInput.style.cssText = ` 1816 | width: 100%; 1817 | padding: 12px; 1818 | border-radius: 8px; 1819 | border: 1px solid #d1d5db; 1820 | resize: vertical; 1821 | font-size: 14px; 1822 | transition: all 0.2s ease; 1823 | margin-top: 10px; 1824 | `; 1825 | 1826 | addFocusBlurEffects(roleInput); 1827 | roleSetting.append(roleInput); 1828 | aiSettingsPanel.append(roleSetting); 1829 | 1830 | const advancedSettingsPanel = document.createElement("div"); 1831 | advancedSettingsPanel.id = "advanced-settings-panel"; 1832 | advancedSettingsPanel.style.display = "none"; 1833 | 1834 | const autoReplySettingResult = createSettingItem( 1835 | "Ai回复模式", 1836 | "开启后Ai将自动回复消息", 1837 | () => document.querySelector("#toggle-auto-reply-mode input") 1838 | ); 1839 | 1840 | const autoReplySetting = autoReplySettingResult.settingItem; 1841 | const autoReplyDescriptionContainer = 1842 | autoReplySettingResult.descriptionContainer; 1843 | 1844 | const autoReplyToggle = createToggleSwitch( 1845 | "auto-reply-mode", 1846 | settings.autoReply, 1847 | (checked) => { 1848 | settings.autoReply = checked; 1849 | } 1850 | ); 1851 | 1852 | autoReplyDescriptionContainer.append(autoReplyToggle); 1853 | 1854 | const autoSendResumeSettingResult = createSettingItem( 1855 | "自动发送附件简历", 1856 | "开启后系统将自动发送附件简历给HR", 1857 | () => document.querySelector("#toggle-auto-send-resume input") 1858 | ); 1859 | 1860 | const autoSendResumeSetting = autoSendResumeSettingResult.settingItem; 1861 | const autoSendResumeDescriptionContainer = 1862 | autoSendResumeSettingResult.descriptionContainer; 1863 | 1864 | const autoSendResumeToggle = createToggleSwitch( 1865 | "auto-send-resume", 1866 | settings.useAutoSendResume, 1867 | (checked) => { 1868 | settings.useAutoSendResume = checked; 1869 | } 1870 | ); 1871 | 1872 | autoSendResumeDescriptionContainer.append(autoSendResumeToggle); 1873 | 1874 | const excludeHeadhuntersSettingResult = createSettingItem( 1875 | "投递时排除猎头", 1876 | "开启后将不会向猎头职位自动投递简历", 1877 | () => document.querySelector("#toggle-exclude-headhunters input") 1878 | ); 1879 | 1880 | const excludeHeadhuntersSetting = 1881 | excludeHeadhuntersSettingResult.settingItem; 1882 | const excludeHeadhuntersDescriptionContainer = 1883 | excludeHeadhuntersSettingResult.descriptionContainer; 1884 | 1885 | const excludeHeadhuntersToggle = createToggleSwitch( 1886 | "exclude-headhunters", 1887 | settings.excludeHeadhunters, 1888 | (checked) => { 1889 | settings.excludeHeadhunters = checked; 1890 | } 1891 | ); 1892 | 1893 | excludeHeadhuntersDescriptionContainer.append(excludeHeadhuntersToggle); 1894 | 1895 | const imageResumeSettingResult = createSettingItem( 1896 | "自动发送图片简历", 1897 | "开启后将发送图片简历给HR(需先选择图片文件)", 1898 | () => document.querySelector("#toggle-auto-send-image-resume input") 1899 | ); 1900 | 1901 | const imageResumeSetting = imageResumeSettingResult.settingItem; 1902 | const imageResumeDescriptionContainer = 1903 | imageResumeSettingResult.descriptionContainer; 1904 | 1905 | if (!state.settings.imageResumes) { 1906 | state.settings.imageResumes = []; 1907 | } 1908 | 1909 | const fileInputContainer = document.createElement("div"); 1910 | fileInputContainer.style.cssText = ` 1911 | display: flex; 1912 | flex-direction: column; 1913 | gap: 10px; 1914 | width: 100%; 1915 | margin-top: 10px; 1916 | `; 1917 | 1918 | const addResumeBtn = document.createElement("button"); 1919 | addResumeBtn.id = "add-image-resume-btn"; 1920 | addResumeBtn.textContent = "添加图片简历"; 1921 | addResumeBtn.style.cssText = ` 1922 | padding: 8px 16px; 1923 | border-radius: 6px; 1924 | border: 1px solid rgba(0, 123, 255, 0.7); 1925 | background: rgba(0, 123, 255, 0.1); 1926 | color: rgba(0, 123, 255, 0.9); 1927 | cursor: pointer; 1928 | font-size: 14px; 1929 | transition: all 0.2s ease; 1930 | align-self: flex-start; 1931 | white-space: nowrap; 1932 | `; 1933 | 1934 | const fileNameDisplay = document.createElement("div"); 1935 | fileNameDisplay.id = "image-resume-filename"; 1936 | fileNameDisplay.style.cssText = ` 1937 | flex: 1; 1938 | padding: 8px; 1939 | border-radius: 6px; 1940 | border: 1px solid #d1d5db; 1941 | background: #f8fafc; 1942 | color: #334155; 1943 | font-size: 14px; 1944 | overflow: hidden; 1945 | white-space: nowrap; 1946 | text-overflow: ellipsis; 1947 | `; 1948 | const resumeCount = state.settings.imageResumes 1949 | ? state.settings.imageResumes.length 1950 | : 0; 1951 | fileNameDisplay.textContent = 1952 | resumeCount > 0 ? `已上传 ${resumeCount} 个简历` : "未选择文件"; 1953 | 1954 | const autoSendImageResumeToggle = (() => { 1955 | const hasImageResumes = 1956 | state.settings.imageResumes && state.settings.imageResumes.length > 0; 1957 | const isValidState = hasImageResumes && settings.useAutoSendImageResume; 1958 | if (!hasImageResumes) settings.useAutoSendImageResume = false; 1959 | 1960 | return createToggleSwitch( 1961 | "auto-send-image-resume", 1962 | isValidState, 1963 | (checked) => { 1964 | if ( 1965 | checked && 1966 | (!state.settings.imageResumes || 1967 | state.settings.imageResumes.length === 0) 1968 | ) { 1969 | showNotification("请先选择图片文件", "error"); 1970 | 1971 | const slider = document.querySelector( 1972 | "#toggle-auto-send-image-resume .toggle-slider" 1973 | ); 1974 | const container = document.querySelector( 1975 | "#toggle-auto-send-image-resume .toggle-switch" 1976 | ); 1977 | 1978 | container.style.backgroundColor = "#e5e7eb"; 1979 | slider.style.transform = "translateX(0)"; 1980 | document.querySelector( 1981 | "#toggle-auto-send-image-resume input" 1982 | ).checked = false; 1983 | } 1984 | settings.useAutoSendImageResume = checked; 1985 | return true; 1986 | } 1987 | ); 1988 | })(); 1989 | 1990 | const hiddenFileInput = document.createElement("input"); 1991 | hiddenFileInput.id = "image-resume-input"; 1992 | hiddenFileInput.type = "file"; 1993 | hiddenFileInput.accept = "image/*"; 1994 | hiddenFileInput.style.display = "none"; 1995 | 1996 | const uploadedResumesContainer = document.createElement("div"); 1997 | uploadedResumesContainer.id = "uploaded-resumes-container"; 1998 | uploadedResumesContainer.style.cssText = ` 1999 | display: flex; 2000 | flex-direction: column; 2001 | gap: 8px; 2002 | width: 100%; 2003 | `; 2004 | 2005 | function renderResumeItem(index, resume) { 2006 | const resumeItem = document.createElement("div"); 2007 | resumeItem.style.cssText = ` 2008 | display: flex; 2009 | align-items: center; 2010 | justify-content: space-between; 2011 | padding: 8px 12px; 2012 | border-radius: 6px; 2013 | background: rgba(0, 0, 0, 0.05); 2014 | font-size: 14px; 2015 | `; 2016 | 2017 | const fileNameSpan = document.createElement("span"); 2018 | fileNameSpan.textContent = resume.path; 2019 | fileNameSpan.style.cssText = ` 2020 | flex: 1; 2021 | white-space: nowrap; 2022 | overflow: hidden; 2023 | text-overflow: ellipsis; 2024 | margin-right: 8px; 2025 | `; 2026 | 2027 | const deleteBtn = document.createElement("button"); 2028 | deleteBtn.textContent = "删除"; 2029 | deleteBtn.style.cssText = ` 2030 | padding: 4px 12px; 2031 | border-radius: 4px; 2032 | border: 1px solid rgba(255, 70, 70, 0.7); 2033 | background: rgba(255, 70, 70, 0.1); 2034 | color: rgba(255, 70, 70, 0.9); 2035 | cursor: pointer; 2036 | font-size: 12px; 2037 | `; 2038 | 2039 | deleteBtn.addEventListener("click", () => { 2040 | state.settings.imageResumes.splice(index, 1); 2041 | 2042 | resumeItem.remove(); 2043 | 2044 | if (state.settings.imageResumes.length === 0) { 2045 | state.settings.useAutoSendImageResume = false; 2046 | const toggleInput = document.querySelector( 2047 | "#toggle-auto-send-image-resume input" 2048 | ); 2049 | if (toggleInput) { 2050 | toggleInput.checked = false; 2051 | toggleInput.dispatchEvent(new Event("change")); 2052 | } 2053 | } 2054 | 2055 | if ( 2056 | typeof StatePersistence !== "undefined" && 2057 | StatePersistence.saveState 2058 | ) { 2059 | StatePersistence.saveState(); 2060 | } 2061 | }); 2062 | 2063 | resumeItem.appendChild(fileNameSpan); 2064 | resumeItem.appendChild(deleteBtn); 2065 | 2066 | return resumeItem; 2067 | } 2068 | 2069 | if (state.settings.imageResumes && state.settings.imageResumes.length > 0) { 2070 | state.settings.imageResumes.forEach((resume, index) => { 2071 | const resumeItem = renderResumeItem(index, resume); 2072 | uploadedResumesContainer.appendChild(resumeItem); 2073 | }); 2074 | } 2075 | 2076 | addResumeBtn.addEventListener("click", () => { 2077 | if (state.settings.imageResumes.length >= 5) { 2078 | if (typeof showNotification !== "undefined") { 2079 | showNotification("免费版最多添加5个图片简历", "info"); 2080 | } else { 2081 | alert("免费版最多添加5个图片简历"); 2082 | } 2083 | } else { 2084 | hiddenFileInput.click(); 2085 | } 2086 | }); 2087 | 2088 | hiddenFileInput.addEventListener("change", (e) => { 2089 | if (e.target.files && e.target.files[0]) { 2090 | const file = e.target.files[0]; 2091 | 2092 | const isDuplicate = state.settings.imageResumes.some( 2093 | (resume) => resume.path === file.name 2094 | ); 2095 | if (isDuplicate) { 2096 | if (typeof showNotification !== "undefined") { 2097 | showNotification("该文件名已存在", "error"); 2098 | } else { 2099 | alert("该文件名已存在"); 2100 | } 2101 | return; 2102 | } 2103 | 2104 | const reader = new FileReader(); 2105 | reader.onload = function (event) { 2106 | const newResume = { 2107 | path: file.name, 2108 | data: event.target.result, 2109 | }; 2110 | 2111 | state.settings.imageResumes.push(newResume); 2112 | 2113 | const index = state.settings.imageResumes.length - 1; 2114 | const resumeItem = renderResumeItem(index, newResume); 2115 | uploadedResumesContainer.appendChild(resumeItem); 2116 | 2117 | if (!state.settings.useAutoSendImageResume) { 2118 | state.settings.useAutoSendImageResume = true; 2119 | const toggleInput = document.querySelector( 2120 | "#toggle-auto-send-image-resume input" 2121 | ); 2122 | if (toggleInput) { 2123 | toggleInput.checked = true; 2124 | toggleInput.dispatchEvent(new Event("change")); 2125 | } 2126 | } 2127 | 2128 | if ( 2129 | typeof StatePersistence !== "undefined" && 2130 | StatePersistence.saveState 2131 | ) { 2132 | StatePersistence.saveState(); 2133 | } 2134 | }; 2135 | reader.readAsDataURL(file); 2136 | } 2137 | }); 2138 | 2139 | fileInputContainer.append( 2140 | addResumeBtn, 2141 | uploadedResumesContainer, 2142 | hiddenFileInput 2143 | ); 2144 | imageResumeDescriptionContainer.append(autoSendImageResumeToggle); 2145 | imageResumeSetting.append(fileInputContainer); 2146 | 2147 | const recruiterStatusSettingResult = createSettingItem( 2148 | "投递招聘者状态", 2149 | "筛选活跃状态符合要求的招聘者进行投递", 2150 | () => document.querySelector("#recruiter-status-select .select-header") 2151 | ); 2152 | 2153 | const recruiterStatusSetting = recruiterStatusSettingResult.settingItem; 2154 | 2155 | const statusSelect = document.createElement("div"); 2156 | statusSelect.id = "recruiter-status-select"; 2157 | statusSelect.className = "custom-select"; 2158 | statusSelect.style.cssText = ` 2159 | position: relative; 2160 | width: 100%; 2161 | margin-top: 10px; 2162 | `; 2163 | 2164 | const statusHeader = document.createElement("div"); 2165 | statusHeader.className = "select-header"; 2166 | statusHeader.style.cssText = ` 2167 | display: flex; 2168 | justify-content: space-between; 2169 | align-items: center; 2170 | padding: 12px 16px; 2171 | border-radius: 8px; 2172 | border: 1px solid #e2e8f0; 2173 | background: white; 2174 | cursor: pointer; 2175 | transition: all 0.2s ease; 2176 | box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05); 2177 | min-height: 44px; 2178 | `; 2179 | 2180 | const statusDisplay = document.createElement("div"); 2181 | statusDisplay.className = "select-value"; 2182 | statusDisplay.style.cssText = ` 2183 | flex: 1; 2184 | text-align: left; 2185 | color: #334155; 2186 | font-size: 14px; 2187 | overflow: hidden; 2188 | white-space: nowrap; 2189 | text-overflow: ellipsis; 2190 | `; 2191 | statusDisplay.textContent = getStatusDisplayText(); 2192 | 2193 | const statusIcon = document.createElement("div"); 2194 | statusIcon.className = "select-icon"; 2195 | statusIcon.innerHTML = "▼"; 2196 | statusIcon.style.cssText = ` 2197 | margin-left: 10px; 2198 | color: #64748b; 2199 | transition: transform 0.2s ease; 2200 | `; 2201 | 2202 | const statusClear = document.createElement("button"); 2203 | statusClear.className = "select-clear"; 2204 | statusClear.innerHTML = "×"; 2205 | statusClear.style.cssText = ` 2206 | background: none; 2207 | border: none; 2208 | color: #94a3b8; 2209 | cursor: pointer; 2210 | font-size: 16px; 2211 | margin-left: 8px; 2212 | display: none; 2213 | transition: color 0.2s ease; 2214 | `; 2215 | 2216 | statusHeader.append(statusDisplay, statusClear, statusIcon); 2217 | 2218 | const statusOptions = document.createElement("div"); 2219 | statusOptions.className = "select-options"; 2220 | statusOptions.style.cssText = ` 2221 | position: absolute; 2222 | top: calc(100% + 6px); 2223 | left: 0; 2224 | right: 0; 2225 | max-height: 240px; 2226 | overflow-y: auto; 2227 | border-radius: 8px; 2228 | border: 1px solid #e2e8f0; 2229 | background: white; 2230 | z-index: 100; 2231 | box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); 2232 | display: none; 2233 | transition: all 0.2s ease; 2234 | scrollbar-width: thin; 2235 | scrollbar-color: #cbd5e1 #f1f5f9; 2236 | `; 2237 | 2238 | statusOptions.innerHTML += ` 2239 | 2255 | `; 2256 | 2257 | const statusOptionsList = [ 2258 | { value: "不限", text: "不限" }, 2259 | { value: "在线", text: "在线" }, 2260 | { value: "刚刚活跃", text: "刚刚活跃" }, 2261 | { value: "今日活跃", text: "今日活跃" }, 2262 | { value: "3日内活跃", text: "3日内活跃" }, 2263 | { value: "本周活跃", text: "本周活跃" }, 2264 | { value: "本月活跃", text: "本月活跃" }, 2265 | { value: "半年前活跃", text: "半年前活跃" }, 2266 | ]; 2267 | 2268 | statusOptionsList.forEach((option) => { 2269 | const statusOption = document.createElement("div"); 2270 | statusOption.className = 2271 | "select-option" + 2272 | (state.settings.recruiterActivityStatus && 2273 | Array.isArray(state.settings.recruiterActivityStatus) && 2274 | state.settings.recruiterActivityStatus.includes(option.value) 2275 | ? " selected" 2276 | : ""); 2277 | statusOption.dataset.value = option.value; 2278 | statusOption.style.cssText = ` 2279 | padding: 12px 16px; 2280 | cursor: pointer; 2281 | transition: all 0.2s ease; 2282 | display: flex; 2283 | align-items: center; 2284 | font-size: 14px; 2285 | color: #334155; 2286 | `; 2287 | 2288 | const checkIcon = document.createElement("span"); 2289 | checkIcon.className = "check-icon"; 2290 | checkIcon.innerHTML = "✓"; 2291 | checkIcon.style.cssText = ` 2292 | margin-right: 8px; 2293 | color: rgba(0, 123, 255, 0.9); 2294 | font-weight: bold; 2295 | display: ${ 2296 | state.settings.recruiterActivityStatus && 2297 | Array.isArray(state.settings.recruiterActivityStatus) && 2298 | state.settings.recruiterActivityStatus.includes(option.value) 2299 | ? "inline" 2300 | : "none" 2301 | }; 2302 | `; 2303 | 2304 | const textSpan = document.createElement("span"); 2305 | textSpan.textContent = option.text; 2306 | 2307 | statusOption.append(checkIcon, textSpan); 2308 | 2309 | statusOption.addEventListener("click", (e) => { 2310 | e.stopPropagation(); 2311 | toggleStatusOption(option.value); 2312 | }); 2313 | 2314 | statusOptions.appendChild(statusOption); 2315 | }); 2316 | 2317 | statusHeader.addEventListener("click", () => { 2318 | statusOptions.style.display = 2319 | statusOptions.style.display === "block" ? "none" : "block"; 2320 | statusIcon.style.transform = 2321 | statusOptions.style.display === "block" 2322 | ? "rotate(180deg)" 2323 | : "rotate(0)"; 2324 | }); 2325 | 2326 | statusClear.addEventListener("click", (e) => { 2327 | e.stopPropagation(); 2328 | state.settings.recruiterActivityStatus = []; 2329 | updateStatusOptions(); 2330 | }); 2331 | 2332 | document.addEventListener("click", (e) => { 2333 | if (!statusSelect.contains(e.target)) { 2334 | statusOptions.style.display = "none"; 2335 | statusIcon.style.transform = "rotate(0)"; 2336 | } 2337 | }); 2338 | 2339 | statusHeader.addEventListener("mouseenter", () => { 2340 | statusHeader.style.borderColor = "rgba(0, 123, 255, 0.5)"; 2341 | statusHeader.style.boxShadow = "0 0 0 3px rgba(0, 123, 255, 0.1)"; 2342 | }); 2343 | 2344 | statusHeader.addEventListener("mouseleave", () => { 2345 | if (!statusHeader.contains(document.activeElement)) { 2346 | statusHeader.style.borderColor = "#e2e8f0"; 2347 | statusHeader.style.boxShadow = "0 1px 2px rgba(0, 0, 0, 0.05)"; 2348 | } 2349 | }); 2350 | 2351 | statusHeader.addEventListener("focus", () => { 2352 | statusHeader.style.borderColor = "rgba(0, 123, 255, 0.7)"; 2353 | statusHeader.style.boxShadow = "0 0 0 3px rgba(0, 123, 255, 0.2)"; 2354 | }); 2355 | 2356 | statusHeader.addEventListener("blur", () => { 2357 | statusHeader.style.borderColor = "#e2e8f0"; 2358 | statusHeader.style.boxShadow = "0 1px 2px rgba(0, 0, 0, 0.05)"; 2359 | }); 2360 | 2361 | statusSelect.append(statusHeader, statusOptions); 2362 | recruiterStatusSetting.append(statusSelect); 2363 | 2364 | advancedSettingsPanel.append( 2365 | autoReplySetting, 2366 | autoSendResumeSetting, 2367 | excludeHeadhuntersSetting, 2368 | imageResumeSetting, 2369 | recruiterStatusSetting 2370 | ); 2371 | 2372 | const intervalSettingsPanel = document.createElement("div"); 2373 | intervalSettingsPanel.id = "interval-settings-panel"; 2374 | intervalSettingsPanel.style.display = "none"; 2375 | 2376 | const basicIntervalSettingResult = createSettingItem( 2377 | "基本间隔", 2378 | "滚动、检查新聊天等间隔时间(毫秒)", 2379 | () => document.getElementById("basic-interval-input") 2380 | ); 2381 | 2382 | const basicIntervalSetting = basicIntervalSettingResult.settingItem; 2383 | 2384 | const basicIntervalInput = document.createElement("input"); 2385 | basicIntervalInput.id = "basic-interval-input"; 2386 | basicIntervalInput.type = "number"; 2387 | basicIntervalInput.min = 500; 2388 | basicIntervalInput.max = 10000; 2389 | basicIntervalInput.step = 100; 2390 | basicIntervalInput.style.cssText = ` 2391 | width: 100%; 2392 | padding: 10px; 2393 | border-radius: 8px; 2394 | border: 1px solid #d1d5db; 2395 | font-size: 14px; 2396 | margin-top: 10px; 2397 | transition: all 0.2s ease; 2398 | `; 2399 | 2400 | addFocusBlurEffects(basicIntervalInput); 2401 | basicIntervalSetting.append(basicIntervalInput); 2402 | 2403 | const operationIntervalSettingResult = createSettingItem( 2404 | "操作间隔", 2405 | "点击沟通按钮之间的间隔时间(毫秒)", 2406 | () => document.getElementById("operation-interval-input") 2407 | ); 2408 | 2409 | const operationIntervalSetting = operationIntervalSettingResult.settingItem; 2410 | 2411 | const operationIntervalInput = document.createElement("input"); 2412 | operationIntervalInput.id = "operation-interval-input"; 2413 | operationIntervalInput.type = "number"; 2414 | operationIntervalInput.min = 100; 2415 | operationIntervalInput.max = 2000; 2416 | operationIntervalInput.step = 50; 2417 | operationIntervalInput.style.cssText = ` 2418 | width: 100%; 2419 | padding: 10px; 2420 | border-radius: 8px; 2421 | border: 1px solid #d1d5db; 2422 | font-size: 14px; 2423 | margin-top: 10px; 2424 | transition: all 0.2s ease; 2425 | `; 2426 | 2427 | addFocusBlurEffects(operationIntervalInput); 2428 | operationIntervalSetting.append(operationIntervalInput); 2429 | 2430 | const scrollSpeedSettingResult = createSettingItem( 2431 | "自动滚动速度", 2432 | "页面自动滚动的速度 (毫秒/像素)", 2433 | () => document.getElementById("scroll-speed-input") 2434 | ); 2435 | 2436 | const scrollSpeedSetting = scrollSpeedSettingResult.settingItem; 2437 | 2438 | const scrollSpeedInput = document.createElement("input"); 2439 | scrollSpeedInput.id = "scroll-speed-input"; 2440 | scrollSpeedInput.type = "number"; 2441 | scrollSpeedInput.min = 100; 2442 | scrollSpeedInput.max = 2000; 2443 | scrollSpeedInput.step = 50; 2444 | scrollSpeedInput.style.cssText = ` 2445 | width: 100%; 2446 | padding: 10px; 2447 | border-radius: 8px; 2448 | border: 1px solid #d1d5db; 2449 | font-size: 14px; 2450 | margin-top: 10px; 2451 | transition: all 0.2s ease; 2452 | `; 2453 | 2454 | addFocusBlurEffects(scrollSpeedInput); 2455 | scrollSpeedSetting.append(scrollSpeedInput); 2456 | 2457 | intervalSettingsPanel.append( 2458 | basicIntervalSetting, 2459 | operationIntervalSetting, 2460 | scrollSpeedSetting 2461 | ); 2462 | 2463 | aiTab.addEventListener("click", () => { 2464 | setActiveTab(aiTab, aiSettingsPanel); 2465 | }); 2466 | 2467 | advancedTab.addEventListener("click", () => { 2468 | setActiveTab(advancedTab, advancedSettingsPanel); 2469 | }); 2470 | 2471 | intervalTab.addEventListener("click", () => { 2472 | setActiveTab(intervalTab, intervalSettingsPanel); 2473 | }); 2474 | 2475 | const dialogFooter = document.createElement("div"); 2476 | dialogFooter.style.cssText = ` 2477 | padding: 15px 20px; 2478 | border-top: 1px solid #e5e7eb; 2479 | display: flex; 2480 | justify-content: flex-end; 2481 | gap: 10px; 2482 | background: rgba(0, 0, 0, 0.03); 2483 | `; 2484 | 2485 | const cancelBtn = createTextButton("取消", "#e5e7eb", () => { 2486 | dialog.style.display = "none"; 2487 | }); 2488 | 2489 | const saveBtn = createTextButton( 2490 | "保存设置", 2491 | "rgba(0, 123, 255, 0.9)", 2492 | () => { 2493 | try { 2494 | const aiRoleInput = document.getElementById("ai-role-input"); 2495 | settings.ai.role = aiRoleInput ? aiRoleInput.value : ""; 2496 | 2497 | const basicIntervalInput = document.getElementById( 2498 | "basic-interval-input" 2499 | ); 2500 | const basicIntervalValue = basicIntervalInput 2501 | ? parseInt(basicIntervalInput.value) 2502 | : settings.intervals.basic; 2503 | settings.intervals.basic = isNaN(basicIntervalValue) 2504 | ? settings.intervals.basic 2505 | : basicIntervalValue; 2506 | 2507 | const operationIntervalInput = document.getElementById( 2508 | "operation-interval-input" 2509 | ); 2510 | const operationIntervalValue = operationIntervalInput 2511 | ? parseInt(operationIntervalInput.value) 2512 | : settings.intervals.operation; 2513 | settings.intervals.operation = isNaN(operationIntervalValue) 2514 | ? settings.intervals.operation 2515 | : operationIntervalValue; 2516 | 2517 | const scrollSpeedInput = 2518 | document.getElementById("scroll-speed-input"); 2519 | const scrollSpeedValue = scrollSpeedInput 2520 | ? parseInt(scrollSpeedInput.value) 2521 | : settings.autoScrollSpeed; 2522 | settings.autoScrollSpeed = isNaN(scrollSpeedValue) 2523 | ? settings.autoScrollSpeed 2524 | : scrollSpeedValue; 2525 | 2526 | saveSettings(); 2527 | 2528 | showNotification("设置已保存"); 2529 | dialog.style.display = "none"; 2530 | } catch (error) { 2531 | showNotification("保存失败: " + error.message, "error"); 2532 | console.error("保存设置失败:", error); 2533 | } 2534 | } 2535 | ); 2536 | 2537 | dialogFooter.append(cancelBtn, saveBtn); 2538 | 2539 | dialogContent.append( 2540 | tabsContainer, 2541 | aiSettingsPanel, 2542 | advancedSettingsPanel, 2543 | intervalSettingsPanel 2544 | ); 2545 | dialog.append(dialogHeader, dialogContent, dialogFooter); 2546 | 2547 | dialog.addEventListener("click", (e) => { 2548 | if (e.target === dialog) { 2549 | dialog.style.display = "none"; 2550 | } 2551 | }); 2552 | 2553 | return dialog; 2554 | } 2555 | 2556 | function showSettingsDialog() { 2557 | let dialog = document.getElementById("boss-settings-dialog"); 2558 | if (!dialog) { 2559 | dialog = createSettingsDialog(); 2560 | document.body.appendChild(dialog); 2561 | } 2562 | 2563 | dialog.style.display = "flex"; 2564 | 2565 | setTimeout(() => { 2566 | dialog.classList.add("active"); 2567 | setTimeout(loadSettingsIntoUI, 100); 2568 | }, 10); 2569 | } 2570 | 2571 | function toggleStatusOption(value) { 2572 | if (value === "不限") { 2573 | settings.recruiterActivityStatus = 2574 | settings.recruiterActivityStatus.includes("不限") ? [] : ["不限"]; 2575 | } else { 2576 | if (settings.recruiterActivityStatus.includes("不限")) { 2577 | settings.recruiterActivityStatus = [value]; 2578 | } else { 2579 | if (settings.recruiterActivityStatus.includes(value)) { 2580 | settings.recruiterActivityStatus = 2581 | settings.recruiterActivityStatus.filter((v) => v !== value); 2582 | } else { 2583 | settings.recruiterActivityStatus.push(value); 2584 | } 2585 | 2586 | if (settings.recruiterActivityStatus.length === 0) { 2587 | settings.recruiterActivityStatus = ["不限"]; 2588 | } 2589 | } 2590 | } 2591 | 2592 | updateStatusOptions(); 2593 | } 2594 | 2595 | function updateStatusOptions() { 2596 | const options = document.querySelectorAll( 2597 | "#recruiter-status-select .select-option" 2598 | ); 2599 | options.forEach((option) => { 2600 | const isSelected = settings.recruiterActivityStatus.includes( 2601 | option.dataset.value 2602 | ); 2603 | option.className = "select-option" + (isSelected ? " selected" : ""); 2604 | option.querySelector(".check-icon").style.display = isSelected 2605 | ? "inline" 2606 | : "none"; 2607 | 2608 | if (option.dataset.value === "不限") { 2609 | if (isSelected) { 2610 | options.forEach((opt) => { 2611 | if (opt.dataset.value !== "不限") { 2612 | opt.className = "select-option"; 2613 | opt.querySelector(".check-icon").style.display = "none"; 2614 | } 2615 | }); 2616 | } 2617 | } 2618 | }); 2619 | 2620 | document.querySelector( 2621 | "#recruiter-status-select .select-value" 2622 | ).textContent = getStatusDisplayText(); 2623 | 2624 | document.querySelector( 2625 | "#recruiter-status-select .select-clear" 2626 | ).style.display = 2627 | settings.recruiterActivityStatus.length > 0 && 2628 | !settings.recruiterActivityStatus.includes("不限") 2629 | ? "inline" 2630 | : "none"; 2631 | } 2632 | 2633 | function getStatusDisplayText() { 2634 | if (settings.recruiterActivityStatus.includes("不限")) { 2635 | return "不限"; 2636 | } 2637 | 2638 | if (settings.recruiterActivityStatus.length === 0) { 2639 | return "请选择"; 2640 | } 2641 | 2642 | if (settings.recruiterActivityStatus.length <= 2) { 2643 | return settings.recruiterActivityStatus.join("、"); 2644 | } 2645 | 2646 | return `${settings.recruiterActivityStatus[0]}、${settings.recruiterActivityStatus[1]}等${settings.recruiterActivityStatus.length}项`; 2647 | } 2648 | 2649 | function loadSettingsIntoUI() { 2650 | const aiRoleInput = document.getElementById("ai-role-input"); 2651 | if (aiRoleInput) { 2652 | aiRoleInput.value = settings.ai.role; 2653 | } 2654 | 2655 | const autoReplyInput = document.querySelector( 2656 | "#toggle-auto-reply-mode input" 2657 | ); 2658 | if (autoReplyInput) { 2659 | autoReplyInput.checked = settings.autoReply; 2660 | } 2661 | 2662 | const autoSendResumeInput = document.querySelector( 2663 | "#toggle-auto-send-resume input" 2664 | ); 2665 | if (autoSendResumeInput) { 2666 | autoSendResumeInput.checked = settings.useAutoSendResume; 2667 | } 2668 | 2669 | const excludeHeadhuntersInput = document.querySelector( 2670 | "#toggle-exclude-headhunters input" 2671 | ); 2672 | if (excludeHeadhuntersInput) { 2673 | excludeHeadhuntersInput.checked = settings.excludeHeadhunters; 2674 | } 2675 | 2676 | const basicIntervalInput = document.getElementById("basic-interval-input"); 2677 | if (basicIntervalInput) { 2678 | basicIntervalInput.value = settings.intervals.basic.toString(); 2679 | } 2680 | 2681 | const operationIntervalInput = document.getElementById( 2682 | "operation-interval-input" 2683 | ); 2684 | if (operationIntervalInput) { 2685 | operationIntervalInput.value = settings.intervals.operation.toString(); 2686 | } 2687 | 2688 | const scrollSpeedInput = document.getElementById("scroll-speed-input"); 2689 | if (scrollSpeedInput) { 2690 | scrollSpeedInput.value = settings.autoScrollSpeed.toString(); 2691 | } 2692 | 2693 | const autoSendImageResumeInput = document.querySelector( 2694 | "#toggle-auto-send-image-resume input" 2695 | ); 2696 | if (autoSendImageResumeInput) { 2697 | autoSendImageResumeInput.checked = 2698 | settings.useAutoSendImageResume && 2699 | settings.imageResumes && 2700 | settings.imageResumes.length > 0; 2701 | } 2702 | 2703 | const communicationModeSelector = document.querySelector( 2704 | "#communication-mode-selector select" 2705 | ); 2706 | if (communicationModeSelector) { 2707 | communicationModeSelector.value = settings.communicationMode; 2708 | } 2709 | 2710 | if (elements.communicationIncludeInput) { 2711 | elements.communicationIncludeInput.value = 2712 | settings.communicationIncludeKeywords || ""; 2713 | } 2714 | 2715 | updateStatusOptions(); 2716 | } 2717 | 2718 | function createDialogHeader(title) { 2719 | const header = document.createElement("div"); 2720 | header.style.cssText = ` 2721 | padding: 9px 16px; 2722 | background: rgba(0, 123, 255, 0.9); 2723 | color: white; 2724 | font-size: 19px; 2725 | font-weight: 500; 2726 | display: flex; 2727 | justify-content: space-between; 2728 | align-items: center; 2729 | position: relative; 2730 | `; 2731 | 2732 | const titleElement = document.createElement("div"); 2733 | titleElement.textContent = title; 2734 | 2735 | const closeBtn = document.createElement("button"); 2736 | closeBtn.innerHTML = "✕"; 2737 | closeBtn.title = "关闭设置"; 2738 | closeBtn.style.cssText = ` 2739 | width: 32px; 2740 | height: 32px; 2741 | background: rgba(255, 255, 255, 0.15); 2742 | color: white; 2743 | border-radius: 50%; 2744 | display: flex; 2745 | justify-content: center; 2746 | align-items: center; 2747 | cursor: pointer; 2748 | transition: all 0.2s ease; 2749 | border: none; 2750 | font-size: 16px; 2751 | `; 2752 | 2753 | closeBtn.addEventListener("mouseenter", () => { 2754 | closeBtn.style.backgroundColor = "rgba(255, 255, 255, 0.25)"; 2755 | }); 2756 | 2757 | closeBtn.addEventListener("mouseleave", () => { 2758 | closeBtn.style.backgroundColor = "rgba(255, 255, 255, 0.15)"; 2759 | }); 2760 | 2761 | closeBtn.addEventListener("click", () => { 2762 | const dialog = document.getElementById("boss-settings-dialog"); 2763 | dialog.style.display = "none"; 2764 | }); 2765 | 2766 | header.append(titleElement, closeBtn); 2767 | return header; 2768 | } 2769 | 2770 | function createSettingItem(title, description, controlGetter) { 2771 | const settingItem = document.createElement("div"); 2772 | settingItem.className = "setting-item"; 2773 | settingItem.style.cssText = ` 2774 | padding: 15px; 2775 | border-radius: 10px; 2776 | margin-bottom: 15px; 2777 | background: white; 2778 | box-shadow: 0 1px 3px rgba(0,0,0,0.05); 2779 | border: 1px solid rgba(0, 123, 255, 0.1); 2780 | display: flex; 2781 | flex-direction: column; 2782 | `; 2783 | 2784 | const titleElement = document.createElement("h4"); 2785 | titleElement.textContent = title; 2786 | titleElement.style.cssText = ` 2787 | margin: 0 0 5px; 2788 | color: #333; 2789 | font-size: 16px; 2790 | font-weight: 500; 2791 | `; 2792 | 2793 | const descElement = document.createElement("p"); 2794 | descElement.textContent = description; 2795 | descElement.style.cssText = ` 2796 | margin: 0; 2797 | color: #666; 2798 | font-size: 13px; 2799 | line-height: 1.4; 2800 | `; 2801 | 2802 | const descriptionContainer = document.createElement("div"); 2803 | descriptionContainer.style.cssText = ` 2804 | display: flex; 2805 | justify-content: space-between; 2806 | align-items: center; 2807 | width: 100%; 2808 | `; 2809 | 2810 | const textContainer = document.createElement("div"); 2811 | textContainer.append(titleElement, descElement); 2812 | 2813 | descriptionContainer.append(textContainer); 2814 | 2815 | settingItem.append(descriptionContainer); 2816 | 2817 | settingItem.addEventListener("click", () => { 2818 | const control = controlGetter(); 2819 | if (control && typeof control.focus === "function") { 2820 | control.focus(); 2821 | } 2822 | }); 2823 | 2824 | return { 2825 | settingItem, 2826 | descriptionContainer, 2827 | }; 2828 | } 2829 | 2830 | function createToggleSwitch(id, isChecked, onChange) { 2831 | const container = document.createElement("div"); 2832 | container.className = "toggle-container"; 2833 | container.style.cssText = 2834 | "display: flex; justify-content: space-between; align-items: center;"; 2835 | 2836 | const switchContainer = document.createElement("div"); 2837 | switchContainer.className = "toggle-switch"; 2838 | switchContainer.style.cssText = ` 2839 | position: relative; 2840 | width: 50px; 2841 | height: 26px; 2842 | border-radius: 13px; 2843 | background-color: ${isChecked ? "rgba(0, 123, 255, 0.9)" : "#e5e7eb"}; 2844 | 2845 | cursor: pointer; 2846 | `; 2847 | 2848 | const checkbox = document.createElement("input"); 2849 | checkbox.type = "checkbox"; 2850 | checkbox.id = `toggle-${id}`; 2851 | checkbox.checked = isChecked; 2852 | checkbox.style.display = "none"; 2853 | 2854 | const slider = document.createElement("span"); 2855 | slider.className = "toggle-slider"; 2856 | slider.style.cssText = ` 2857 | position: absolute; 2858 | top: 3px; 2859 | left: ${isChecked ? "27px" : "3px"}; 2860 | width: 20px; 2861 | height: 20px; 2862 | border-radius: 50%; 2863 | background-color: white; 2864 | box-shadow: 0 1px 3px rgba(0,0,0,0.2); 2865 | transition: none; 2866 | `; 2867 | 2868 | const forceUpdateUI = (checked) => { 2869 | checkbox.checked = checked; 2870 | switchContainer.style.backgroundColor = checked 2871 | ? "rgba(0, 123, 255, 0.9)" 2872 | : "#e5e7eb"; 2873 | slider.style.left = checked ? "27px" : "3px"; 2874 | }; 2875 | 2876 | checkbox.addEventListener("change", () => { 2877 | let allowChange = true; 2878 | 2879 | if (onChange) { 2880 | allowChange = onChange(checkbox.checked) !== false; 2881 | } 2882 | 2883 | if (!allowChange) { 2884 | forceUpdateUI(!checkbox.checked); 2885 | return; 2886 | } 2887 | 2888 | forceUpdateUI(checkbox.checked); 2889 | }); 2890 | 2891 | switchContainer.addEventListener("click", () => { 2892 | const newState = !checkbox.checked; 2893 | 2894 | if (onChange) { 2895 | if (onChange(newState) !== false) { 2896 | forceUpdateUI(newState); 2897 | } 2898 | } else { 2899 | forceUpdateUI(newState); 2900 | } 2901 | }); 2902 | 2903 | switchContainer.append(checkbox, slider); 2904 | container.append(switchContainer); 2905 | 2906 | return container; 2907 | } 2908 | 2909 | function createTextButton(text, backgroundColor, onClick) { 2910 | const button = document.createElement("button"); 2911 | button.textContent = text; 2912 | button.style.cssText = ` 2913 | padding: 9px 18px; 2914 | border-radius: 8px; 2915 | border: none; 2916 | font-size: 14px; 2917 | font-weight: 500; 2918 | cursor: pointer; 2919 | transition: all 0.2s ease; 2920 | background: ${backgroundColor}; 2921 | color: white; 2922 | `; 2923 | 2924 | button.addEventListener("click", onClick); 2925 | 2926 | return button; 2927 | } 2928 | 2929 | function addFocusBlurEffects(element) { 2930 | element.addEventListener("focus", () => { 2931 | element.style.borderColor = "rgba(0, 123, 255, 0.7)"; 2932 | element.style.boxShadow = "0 0 0 3px rgba(0, 123, 255, 0.2)"; 2933 | }); 2934 | 2935 | element.addEventListener("blur", () => { 2936 | element.style.borderColor = "#d1d5db"; 2937 | element.style.boxShadow = "none"; 2938 | }); 2939 | } 2940 | 2941 | function setActiveTab(tab, panel) { 2942 | const tabs = document.querySelectorAll(".settings-tab"); 2943 | const panels = [ 2944 | document.getElementById("ai-settings-panel"), 2945 | document.getElementById("advanced-settings-panel"), 2946 | document.getElementById("interval-settings-panel"), 2947 | ]; 2948 | 2949 | tabs.forEach((t) => { 2950 | t.classList.remove("active"); 2951 | t.style.backgroundColor = "rgba(0, 0, 0, 0.05)"; 2952 | t.style.color = "#333"; 2953 | }); 2954 | 2955 | panels.forEach((p) => { 2956 | p.style.display = "none"; 2957 | }); 2958 | 2959 | tab.classList.add("active"); 2960 | tab.style.backgroundColor = "rgba(0, 123, 255, 0.9)"; 2961 | tab.style.color = "white"; 2962 | 2963 | panel.style.display = "block"; 2964 | } 2965 | 2966 | function showNotification(message, type = "success") { 2967 | const notification = document.createElement("div"); 2968 | const bgColor = 2969 | type === "success" ? "rgba(40, 167, 69, 0.9)" : "rgba(220, 53, 69, 0.9)"; 2970 | 2971 | notification.style.cssText = ` 2972 | position: fixed; 2973 | top: 20px; 2974 | left: 50%; 2975 | transform: translateX(-50%); 2976 | background: ${bgColor}; 2977 | color: white; 2978 | padding: 10px 15px; 2979 | border-radius: 8px; 2980 | box-shadow: 0 4px 10px rgba(0,0,0,0.2); 2981 | z-index: 9999999; 2982 | opacity: 0; 2983 | transition: opacity 0.3s ease; 2984 | `; 2985 | 2986 | notification.textContent = message; 2987 | document.body.appendChild(notification); 2988 | 2989 | setTimeout(() => (notification.style.opacity = "1"), 10); 2990 | setTimeout(() => { 2991 | notification.style.opacity = "0"; 2992 | setTimeout(() => document.body.removeChild(notification), 300); 2993 | }, 2000); 2994 | } 2995 | 2996 | function filterJobsByKeywords(jobDescriptions) { 2997 | const excludeKeywords = []; 2998 | const includeKeywords = []; 2999 | 3000 | return jobDescriptions.filter((description) => { 3001 | for (const keyword of excludeKeywords) { 3002 | if (description.includes(keyword)) { 3003 | return false; 3004 | } 3005 | } 3006 | 3007 | if (includeKeywords.length > 0) { 3008 | return includeKeywords.some((keyword) => description.includes(keyword)); 3009 | } 3010 | 3011 | return true; 3012 | }); 3013 | } 3014 | 3015 | const Core = { 3016 | CONFIG, 3017 | 3018 | basicInterval: 3019 | parseInt(localStorage.getItem("basicInterval")) || CONFIG.BASIC_INTERVAL, 3020 | operationInterval: 3021 | parseInt(localStorage.getItem("operationInterval")) || 3022 | CONFIG.OPERATION_INTERVAL, 3023 | messageObserver: null, 3024 | lastProcessedMessage: null, 3025 | processingMessage: false, 3026 | 3027 | domCache: {}, 3028 | 3029 | getCachedElement(selector, forceRefresh = false) { 3030 | if (forceRefresh || !this.domCache[selector]) { 3031 | this.domCache[selector] = document.querySelector(selector); 3032 | } 3033 | return this.domCache[selector]; 3034 | }, 3035 | 3036 | getCachedElements(selector, forceRefresh = false) { 3037 | if (forceRefresh || !this.domCache[selector + "[]"]) { 3038 | this.domCache[selector + "[]"] = document.querySelectorAll(selector); 3039 | } 3040 | return this.domCache[selector + "[]"]; 3041 | }, 3042 | 3043 | clearDomCache() { 3044 | this.domCache = {}; 3045 | }, 3046 | 3047 | async startProcessing() { 3048 | if (location.pathname.includes("/jobs")) await this.autoScrollJobList(); 3049 | 3050 | while (state.isRunning) { 3051 | if (location.pathname.includes("/jobs")) await this.processJobList(); 3052 | else if (location.pathname.includes("/chat")) 3053 | await this.handleChatPage(); 3054 | await this.delay(this.basicInterval); 3055 | } 3056 | }, 3057 | 3058 | async autoScrollJobList() { 3059 | return new Promise((resolve) => { 3060 | const cardSelector = "li.job-card-box"; 3061 | const maxHistory = 3; 3062 | const waitTime = this.basicInterval; 3063 | let cardCountHistory = []; 3064 | let isStopped = false; 3065 | 3066 | const scrollStep = async () => { 3067 | if (isStopped) return; 3068 | 3069 | window.scrollTo({ 3070 | top: document.documentElement.scrollHeight, 3071 | behavior: "smooth", 3072 | }); 3073 | await this.delay(waitTime); 3074 | 3075 | const cards = document.querySelectorAll(cardSelector); 3076 | const currentCount = cards.length; 3077 | cardCountHistory.push(currentCount); 3078 | 3079 | if (cardCountHistory.length > maxHistory) cardCountHistory.shift(); 3080 | 3081 | if ( 3082 | cardCountHistory.length === maxHistory && 3083 | new Set(cardCountHistory).size === 1 3084 | ) { 3085 | this.log("当前页面岗位加载完成,开始沟通"); 3086 | resolve(cards); 3087 | return; 3088 | } 3089 | 3090 | scrollStep(); 3091 | }; 3092 | 3093 | scrollStep(); 3094 | 3095 | this.stopAutoScroll = () => { 3096 | isStopped = true; 3097 | resolve(null); 3098 | }; 3099 | }); 3100 | }, 3101 | 3102 | async processJobList() { 3103 | const excludeHeadhunters = JSON.parse( 3104 | localStorage.getItem("excludeHeadhunters") || "false" 3105 | ); 3106 | const activeStatusFilter = JSON.parse( 3107 | localStorage.getItem("recruiterActivityStatus") || '["不限"]' 3108 | ); 3109 | 3110 | state.jobList = Array.from( 3111 | document.querySelectorAll("li.job-card-box") 3112 | ).filter((card) => { 3113 | const title = 3114 | card.querySelector(".job-name")?.textContent?.toLowerCase() || ""; 3115 | 3116 | const addressText = ( 3117 | card.querySelector(".job-address-desc")?.textContent || 3118 | card.querySelector(".company-location")?.textContent || 3119 | card.querySelector(".job-area")?.textContent || 3120 | "" 3121 | ) 3122 | .toLowerCase() 3123 | .trim(); 3124 | const headhuntingElement = card.querySelector(".job-tag-icon"); 3125 | const altText = headhuntingElement ? headhuntingElement.alt : ""; 3126 | 3127 | const includeMatch = 3128 | state.includeKeywords.length === 0 || 3129 | state.includeKeywords.some((kw) => kw && title.includes(kw.trim())); 3130 | 3131 | const locationMatch = 3132 | state.locationKeywords.length === 0 || 3133 | state.locationKeywords.some( 3134 | (kw) => kw && addressText.includes(kw.trim()) 3135 | ); 3136 | 3137 | const excludeHeadhunterMatch = 3138 | !excludeHeadhunters || !altText.includes("猎头"); 3139 | 3140 | return includeMatch && locationMatch && excludeHeadhunterMatch; 3141 | }); 3142 | 3143 | if (!state.jobList.length) { 3144 | this.log("没有符合条件的职位"); 3145 | toggleProcess(); 3146 | return; 3147 | } 3148 | 3149 | if (state.currentIndex >= state.jobList.length) { 3150 | this.resetCycle(); 3151 | return; 3152 | } 3153 | 3154 | const currentCard = state.jobList[state.currentIndex]; 3155 | currentCard.scrollIntoView({ behavior: "smooth", block: "center" }); 3156 | currentCard.click(); 3157 | 3158 | await this.delay(this.operationInterval * 2); 3159 | 3160 | let activeTime = "未知"; 3161 | const onlineTag = document.querySelector(".boss-online-tag"); 3162 | if (onlineTag && onlineTag.textContent.trim() === "在线") { 3163 | activeTime = "在线"; 3164 | } else { 3165 | const activeTimeElement = document.querySelector(".boss-active-time"); 3166 | activeTime = activeTimeElement?.textContent?.trim() || "未知"; 3167 | } 3168 | 3169 | const isActiveStatusMatch = 3170 | activeStatusFilter.includes("不限") || 3171 | activeStatusFilter.includes(activeTime); 3172 | 3173 | if (!isActiveStatusMatch) { 3174 | this.log(`跳过: 招聘者状态 "${activeTime}"`); 3175 | state.currentIndex++; 3176 | return; 3177 | } 3178 | 3179 | const includeLog = state.includeKeywords.length 3180 | ? `职位名包含[${state.includeKeywords.join("、")}]` 3181 | : "职位名不限"; 3182 | const locationLog = state.locationKeywords.length 3183 | ? `工作地包含[${state.locationKeywords.join("、")}]` 3184 | : "工作地不限"; 3185 | this.log( 3186 | `正在沟通:${++state.currentIndex}/${ 3187 | state.jobList.length 3188 | },${includeLog},${locationLog},招聘者"${activeTime}"` 3189 | ); 3190 | 3191 | const chatBtn = document.querySelector("a.op-btn-chat"); 3192 | if (chatBtn) { 3193 | const btnText = chatBtn.textContent.trim(); 3194 | if (btnText === "立即沟通") { 3195 | chatBtn.click(); 3196 | await this.handleGreetingModal(); 3197 | } 3198 | } 3199 | }, 3200 | 3201 | async handleGreetingModal() { 3202 | await this.delay(this.operationInterval * 4); 3203 | 3204 | const btn = [ 3205 | ...document.querySelectorAll(".default-btn.cancel-btn"), 3206 | ].find((b) => b.textContent.trim() === "留在此页"); 3207 | 3208 | if (btn) { 3209 | btn.click(); 3210 | await this.delay(this.operationInterval * 2); 3211 | } 3212 | }, 3213 | 3214 | async handleChatPage() { 3215 | this.resetMessageState(); 3216 | 3217 | if (this.messageObserver) { 3218 | this.messageObserver.disconnect(); 3219 | this.messageObserver = null; 3220 | } 3221 | 3222 | const latestChatLi = await this.waitForElement(this.getLatestChatLi); 3223 | if (!latestChatLi) return; 3224 | 3225 | const nameEl = latestChatLi.querySelector(".name-text"); 3226 | const companyEl = latestChatLi.querySelector( 3227 | ".name-box span:nth-child(2)" 3228 | ); 3229 | const name = (nameEl?.textContent || "未知").trim(); 3230 | const company = (companyEl?.textContent || "").trim(); 3231 | const hrKey = `${name}-${company}`.toLowerCase(); 3232 | 3233 | if ( 3234 | settings.communicationIncludeKeywords && 3235 | settings.communicationIncludeKeywords.trim() 3236 | ) { 3237 | await this.simulateClick(latestChatLi.querySelector(".figure")); 3238 | await this.delay(this.operationInterval * 2); 3239 | 3240 | const positionName = this.getPositionNameFromChat(); 3241 | const includeKeywords = settings.communicationIncludeKeywords 3242 | .toLowerCase() 3243 | .split(",") 3244 | .map((kw) => kw.trim()) 3245 | .filter((kw) => kw.length > 0); 3246 | 3247 | const positionNameLower = positionName.toLowerCase(); 3248 | const isMatch = includeKeywords.some((keyword) => 3249 | positionNameLower.includes(keyword) 3250 | ); 3251 | 3252 | if (!isMatch) { 3253 | this.log(`跳过岗位,不含关键词[${includeKeywords.join(", ")}]`); 3254 | 3255 | if (settings.communicationMode === "auto") { 3256 | await this.scrollUserList(); 3257 | } 3258 | return; 3259 | } 3260 | } 3261 | 3262 | if (!latestChatLi.classList.contains("last-clicked")) { 3263 | await this.simulateClick(latestChatLi.querySelector(".figure")); 3264 | latestChatLi.classList.add("last-clicked"); 3265 | 3266 | await this.delay(this.operationInterval); 3267 | await HRInteractionManager.handleHRInteraction(hrKey); 3268 | 3269 | if (settings.communicationMode === "auto") { 3270 | await this.scrollUserList(); 3271 | } 3272 | } 3273 | 3274 | await this.setupMessageObserver(hrKey); 3275 | }, 3276 | 3277 | resetMessageState() { 3278 | this.lastProcessedMessage = null; 3279 | this.processingMessage = false; 3280 | }, 3281 | 3282 | async setupMessageObserver(hrKey) { 3283 | const chatContainer = await this.waitForElement(".chat-message .im-list"); 3284 | if (!chatContainer) return; 3285 | 3286 | this.messageObserver = new MutationObserver(async (mutations) => { 3287 | let hasNewFriendMessage = false; 3288 | for (const mutation of mutations) { 3289 | if (mutation.type === "childList" && mutation.addedNodes.length > 0) { 3290 | hasNewFriendMessage = Array.from(mutation.addedNodes).some((node) => 3291 | node.classList?.contains("item-friend") 3292 | ); 3293 | if (hasNewFriendMessage) break; 3294 | } 3295 | } 3296 | 3297 | if (hasNewFriendMessage) { 3298 | await this.handleNewMessage(hrKey); 3299 | } 3300 | }); 3301 | 3302 | this.messageObserver.observe(chatContainer, { 3303 | childList: true, 3304 | subtree: true, 3305 | }); 3306 | }, 3307 | 3308 | async handleNewMessage(hrKey) { 3309 | if (!state.isRunning) return; 3310 | if (this.processingMessage) return; 3311 | 3312 | this.processingMessage = true; 3313 | 3314 | try { 3315 | await this.delay(this.operationInterval); 3316 | 3317 | const lastMessage = await this.getLastFriendMessageText(); 3318 | if (!lastMessage) return; 3319 | 3320 | const cleanedMessage = this.cleanMessage(lastMessage); 3321 | const shouldSendResumeOnly = cleanedMessage.includes("简历"); 3322 | 3323 | if (cleanedMessage === this.lastProcessedMessage) return; 3324 | 3325 | this.lastProcessedMessage = cleanedMessage; 3326 | this.log(`对方: ${lastMessage}`); 3327 | 3328 | await this.delay(CONFIG.DELAYS.MEDIUM_SHORT); 3329 | const updatedMessage = await this.getLastFriendMessageText(); 3330 | if ( 3331 | updatedMessage && 3332 | this.cleanMessage(updatedMessage) !== cleanedMessage 3333 | ) { 3334 | await this.handleNewMessage(hrKey); 3335 | return; 3336 | } 3337 | 3338 | const autoSendResume = JSON.parse( 3339 | localStorage.getItem("useAutoSendResume") || "true" 3340 | ); 3341 | const autoReplyEnabled = JSON.parse( 3342 | localStorage.getItem("autoReply") || "true" 3343 | ); 3344 | 3345 | if (shouldSendResumeOnly && autoSendResume) { 3346 | this.log('对方提到"简历",正在发送简历'); 3347 | const sent = await HRInteractionManager.sendResume(); 3348 | if (sent) { 3349 | state.hrInteractions.sentResumeHRs.add(hrKey); 3350 | StatePersistence.saveState(); 3351 | this.log(`已向 ${hrKey} 发送简历`); 3352 | } 3353 | } else if (autoReplyEnabled) { 3354 | await HRInteractionManager.handleHRInteraction(hrKey); 3355 | } 3356 | 3357 | await this.delay(CONFIG.DELAYS.MEDIUM_SHORT); 3358 | const postReplyMessage = await this.getLastFriendMessageText(); 3359 | } catch (error) { 3360 | this.log(`处理消息出错: ${error.message}`); 3361 | } finally { 3362 | this.processingMessage = false; 3363 | } 3364 | }, 3365 | 3366 | cleanMessage(message) { 3367 | if (!message) return ""; 3368 | 3369 | let clean = message.replace(/<[^>]*>/g, ""); 3370 | clean = clean 3371 | .trim() 3372 | .replace(/\s+/g, " ") 3373 | .replace(/[\u200B-\u200D\uFEFF]/g, ""); 3374 | return clean; 3375 | }, 3376 | 3377 | getLatestChatLi() { 3378 | return document.querySelector( 3379 | 'li[role="listitem"][class]:has(.friend-content-warp)' 3380 | ); 3381 | }, 3382 | 3383 | getPositionNameFromChat() { 3384 | try { 3385 | const positionNameElement = 3386 | Core.getCachedElement(".position-name", true) || 3387 | Core.getCachedElement(".job-name", true) || 3388 | Core.getCachedElement( 3389 | '[class*="position-content"] .left-content .position-name', 3390 | true 3391 | ) || 3392 | document.querySelector(".position-name") || 3393 | document.querySelector(".job-name"); 3394 | 3395 | if (positionNameElement) { 3396 | return positionNameElement.textContent.trim(); 3397 | } else { 3398 | Core.log("未找到岗位名称元素"); 3399 | return ""; 3400 | } 3401 | } catch (e) { 3402 | Core.log(`获取岗位名称出错: ${e.message}`); 3403 | return ""; 3404 | } 3405 | }, 3406 | 3407 | async aiReply() { 3408 | if (!state.isRunning) return; 3409 | try { 3410 | const autoReplyEnabled = JSON.parse( 3411 | localStorage.getItem("autoReply") || "true" 3412 | ); 3413 | if (!autoReplyEnabled) { 3414 | return false; 3415 | } 3416 | 3417 | if (!state.ai.useAiReply) { 3418 | return false; 3419 | } 3420 | 3421 | const lastMessage = await this.getLastFriendMessageText(); 3422 | if (!lastMessage) return false; 3423 | 3424 | const today = new Date().toISOString().split("T")[0]; 3425 | if (state.ai.lastAiDate !== today) { 3426 | state.ai.replyCount = 0; 3427 | state.ai.lastAiDate = today; 3428 | StatePersistence.saveState(); 3429 | } 3430 | 3431 | const maxReplies = state.user.isPremiumUser ? 25 : 10; 3432 | if (state.ai.replyCount >= maxReplies) { 3433 | this.log(`免费版AI回复已达上限`); 3434 | return false; 3435 | } 3436 | 3437 | const aiReplyText = await this.requestAi(lastMessage); 3438 | if (!aiReplyText) return false; 3439 | 3440 | this.log(`AI回复: ${aiReplyText.slice(0, 30)}...`); 3441 | state.ai.replyCount++; 3442 | StatePersistence.saveState(); 3443 | 3444 | const inputBox = await this.waitForElement("#chat-input"); 3445 | if (!inputBox) return false; 3446 | 3447 | inputBox.textContent = ""; 3448 | inputBox.focus(); 3449 | document.execCommand("insertText", false, aiReplyText); 3450 | await this.delay(this.operationInterval / 10); 3451 | 3452 | const sendButton = document.querySelector(".btn-send"); 3453 | if (sendButton) { 3454 | await this.simulateClick(sendButton); 3455 | } else { 3456 | const enterKeyEvent = new KeyboardEvent("keydown", { 3457 | key: "Enter", 3458 | keyCode: 13, 3459 | code: "Enter", 3460 | which: 13, 3461 | bubbles: true, 3462 | }); 3463 | inputBox.dispatchEvent(enterKeyEvent); 3464 | } 3465 | 3466 | return true; 3467 | } catch (error) { 3468 | this.log(`AI回复出错: ${error.message}`); 3469 | return false; 3470 | } 3471 | }, 3472 | 3473 | async requestAi(message) { 3474 | const authToken = (function () { 3475 | const c = [ 3476 | 0x73, 0x64, 0x56, 0x45, 0x44, 0x41, 0x42, 0x6a, 0x5a, 0x65, 0x49, 3477 | 0x6b, 0x77, 0x58, 0x4e, 0x42, 0x46, 0x4e, 0x42, 0x73, 0x3a, 0x43, 3478 | 0x71, 0x4d, 0x58, 0x6a, 0x71, 0x65, 0x50, 0x56, 0x43, 0x4a, 0x62, 3479 | 0x55, 0x59, 0x4a, 0x50, 0x63, 0x69, 0x70, 0x4a, 3480 | ]; 3481 | return c.map((d) => String.fromCharCode(d)).join(""); 3482 | })(); 3483 | 3484 | const apiUrl = (function () { 3485 | const e = 3486 | "68747470733a2f2f737061726b2d6170692d6f70656e2e78662d79756e2e636f6d2f76312f636861742f636f6d706c6574696f6e73"; 3487 | return e.replace(/../g, (f) => String.fromCharCode(parseInt(f, 16))); 3488 | })(); 3489 | 3490 | const requestBody = { 3491 | model: "lite", 3492 | messages: [ 3493 | { 3494 | role: "system", 3495 | content: 3496 | localStorage.getItem("aiRole") || 3497 | "你是有经验的求职者,你会用口语化的表达(如“行”、“呃”)和语气词(如“啊”、“吗”)使对话自然。你回复对方很肯定且言简意赅,不会发送段落和长句子。", 3498 | }, 3499 | { role: "user", content: message }, 3500 | ], 3501 | temperature: 0.9, 3502 | top_p: 0.8, 3503 | max_tokens: 512, 3504 | }; 3505 | 3506 | return new Promise((resolve, reject) => { 3507 | GM_xmlhttpRequest({ 3508 | method: "POST", 3509 | url: apiUrl, 3510 | headers: { 3511 | "Content-Type": "application/json", 3512 | Authorization: "Bearer " + authToken, 3513 | }, 3514 | data: JSON.stringify(requestBody), 3515 | onload: (response) => { 3516 | try { 3517 | const result = JSON.parse(response.responseText); 3518 | if (result.code !== 0) 3519 | throw new Error( 3520 | "API错误: " + result.message + "(Code: " + result.code + ")" 3521 | ); 3522 | resolve(result.choices[0].message.content.trim()); 3523 | } catch (error) { 3524 | reject( 3525 | new Error( 3526 | "响应解析失败: " + 3527 | error.message + 3528 | "\n原始响应: " + 3529 | response.responseText 3530 | ) 3531 | ); 3532 | } 3533 | }, 3534 | onerror: (error) => reject(new Error("网络请求失败: " + error)), 3535 | }); 3536 | }); 3537 | }, 3538 | 3539 | async getLastFriendMessageText() { 3540 | try { 3541 | const chatContainer = document.querySelector(".chat-message .im-list"); 3542 | if (!chatContainer) return null; 3543 | 3544 | const friendMessages = Array.from( 3545 | chatContainer.querySelectorAll("li.message-item.item-friend") 3546 | ); 3547 | if (friendMessages.length === 0) return null; 3548 | 3549 | const lastMessageEl = friendMessages[friendMessages.length - 1]; 3550 | const textEl = lastMessageEl.querySelector(".text span"); 3551 | return textEl?.textContent?.trim() || null; 3552 | } catch (error) { 3553 | this.log(`获取消息出错: ${error.message}`); 3554 | return null; 3555 | } 3556 | }, 3557 | 3558 | async simulateClick(element) { 3559 | if (!element) return; 3560 | 3561 | const rect = element.getBoundingClientRect(); 3562 | const x = rect.left + rect.width / 2; 3563 | const y = rect.top + rect.height / 2; 3564 | 3565 | const dispatchMouseEvent = (type, options = {}) => { 3566 | const event = new MouseEvent(type, { 3567 | bubbles: true, 3568 | cancelable: true, 3569 | view: document.defaultView, 3570 | clientX: x, 3571 | clientY: y, 3572 | ...options, 3573 | }); 3574 | element.dispatchEvent(event); 3575 | }; 3576 | 3577 | dispatchMouseEvent("mouseover"); 3578 | await this.delay(CONFIG.DELAYS.SHORT); 3579 | dispatchMouseEvent("mousemove"); 3580 | await this.delay(CONFIG.DELAYS.SHORT); 3581 | dispatchMouseEvent("mousedown", { button: 0 }); 3582 | await this.delay(CONFIG.DELAYS.SHORT); 3583 | dispatchMouseEvent("mouseup", { button: 0 }); 3584 | await this.delay(CONFIG.DELAYS.SHORT); 3585 | dispatchMouseEvent("click", { button: 0 }); 3586 | }, 3587 | 3588 | async waitForElement(selectorOrFunction, timeout = 5000) { 3589 | return new Promise((resolve) => { 3590 | let element; 3591 | if (typeof selectorOrFunction === "function") 3592 | element = selectorOrFunction(); 3593 | else element = document.querySelector(selectorOrFunction); 3594 | 3595 | if (element) return resolve(element); 3596 | 3597 | const timeoutId = setTimeout(() => { 3598 | observer.disconnect(); 3599 | resolve(null); 3600 | }, timeout); 3601 | const observer = new MutationObserver(() => { 3602 | if (typeof selectorOrFunction === "function") 3603 | element = selectorOrFunction(); 3604 | else element = document.querySelector(selectorOrFunction); 3605 | if (element) { 3606 | clearTimeout(timeoutId); 3607 | observer.disconnect(); 3608 | resolve(element); 3609 | } 3610 | }); 3611 | observer.observe(document.body, { childList: true, subtree: true }); 3612 | }); 3613 | }, 3614 | 3615 | getContextMultiplier(context) { 3616 | const multipliers = { 3617 | dict_load: 1.0, 3618 | click: 0.8, 3619 | selection: 0.8, 3620 | default: 1.0, 3621 | }; 3622 | return multipliers[context] || multipliers["default"]; 3623 | }, 3624 | 3625 | async smartDelay(baseTime, context = "default") { 3626 | const multiplier = this.getContextMultiplier(context); 3627 | const adjustedTime = baseTime * multiplier; 3628 | return this.delay(adjustedTime); 3629 | }, 3630 | 3631 | async delay(ms) { 3632 | return new Promise((resolve) => setTimeout(resolve, ms)); 3633 | }, 3634 | 3635 | /** 3636 | * Extract two-character keywords from text 3637 | * @param {string} text - The text to extract keywords from 3638 | * @returns {Array} Array of two-character keywords 3639 | */ 3640 | extractTwoCharKeywords(text) { 3641 | const keywords = []; 3642 | const cleanedText = text.replace(/[\s,,.。::;;""''\[\]\(\)\{\}]/g, ""); 3643 | 3644 | for (let i = 0; i < cleanedText.length - 1; i++) { 3645 | keywords.push(cleanedText.substring(i, i + 2)); 3646 | } 3647 | 3648 | return keywords; 3649 | }, 3650 | 3651 | resetCycle() { 3652 | toggleProcess(); 3653 | this.log("所有岗位沟通完成,恭喜您即将找到理想工作!"); 3654 | state.currentIndex = 0; 3655 | state.operation.lastMessageTime = 0; 3656 | }, 3657 | 3658 | log(message) { 3659 | const logEntry = `[${new Date().toLocaleTimeString()}] ${message}`; 3660 | const logPanel = document.querySelector("#pro-log"); 3661 | if (logPanel) { 3662 | const logItem = document.createElement("div"); 3663 | logItem.className = "log-item"; 3664 | logItem.textContent = logEntry; 3665 | logPanel.appendChild(logItem); 3666 | logPanel.scrollTop = logPanel.scrollHeight; 3667 | } 3668 | }, 3669 | }; 3670 | 3671 | function toggleProcess() { 3672 | state.isRunning = !state.isRunning; 3673 | 3674 | if (state.isRunning) { 3675 | state.includeKeywords = elements.includeInput.value 3676 | .trim() 3677 | .toLowerCase() 3678 | .split(",") 3679 | .filter((keyword) => keyword.trim() !== ""); 3680 | state.locationKeywords = (elements.locationInput?.value || "") 3681 | .trim() 3682 | .toLowerCase() 3683 | .split(",") 3684 | .filter((keyword) => keyword.trim() !== ""); 3685 | 3686 | elements.controlBtn.textContent = "停止海投"; 3687 | elements.controlBtn.style.background = `linear-gradient(45deg, ${CONFIG.COLORS.SECONDARY}, #f44336)`; 3688 | 3689 | const startTime = new Date(); 3690 | Core.log(`开始自动海投,时间:${startTime.toLocaleTimeString()}`); 3691 | Core.log( 3692 | `筛选条件:职位名包含【${ 3693 | state.includeKeywords.join("、") || "无" 3694 | }】,工作地包含【${state.locationKeywords.join("、") || "无"}】` 3695 | ); 3696 | 3697 | Core.startProcessing(); 3698 | } else { 3699 | elements.controlBtn.textContent = "启动海投"; 3700 | elements.controlBtn.style.background = `linear-gradient(45deg, ${CONFIG.COLORS.PRIMARY}, #4db6ac)`; 3701 | 3702 | state.isRunning = false; 3703 | 3704 | const stopTime = new Date(); 3705 | Core.log(`停止自动海投,时间:${stopTime.toLocaleTimeString()}`); 3706 | Core.log(`本次共沟通 ${state.currentIndex} 个岗位`); 3707 | 3708 | state.currentIndex = 0; 3709 | } 3710 | } 3711 | 3712 | function toggleChatProcess() { 3713 | state.isRunning = !state.isRunning; 3714 | 3715 | if (state.isRunning) { 3716 | elements.controlBtn.textContent = "停止智能聊天"; 3717 | elements.controlBtn.style.background = `linear-gradient(45deg, ${CONFIG.COLORS.SECONDARY}, #f44336)`; 3718 | 3719 | const startTime = new Date(); 3720 | Core.log(`开始智能聊天,时间:${startTime.toLocaleTimeString()}`); 3721 | 3722 | Core.startProcessing(); 3723 | } else { 3724 | elements.controlBtn.textContent = "开始智能聊天"; 3725 | elements.controlBtn.style.background = `linear-gradient(45deg, ${CONFIG.COLORS.PRIMARY}, #4db6ac)`; 3726 | 3727 | state.isRunning = false; 3728 | 3729 | if (Core.messageObserver) { 3730 | Core.messageObserver.disconnect(); 3731 | Core.messageObserver = null; 3732 | } 3733 | 3734 | const stopTime = new Date(); 3735 | Core.log(`停止智能聊天,时间:${stopTime.toLocaleTimeString()}`); 3736 | } 3737 | } 3738 | 3739 | const letter = { 3740 | showLetterToUser: function () { 3741 | const COLORS = { 3742 | primary: "#4285f4", 3743 | primaryDark: "#1967d2", 3744 | accent: "#e8f0fe", 3745 | text: "#333", 3746 | textLight: "#666", 3747 | background: "#f8f9fa", 3748 | }; 3749 | 3750 | const overlay = document.createElement("div"); 3751 | overlay.id = "letter-overlay"; 3752 | overlay.style.cssText = ` 3753 | position: fixed; 3754 | top: 0; 3755 | left: 0; 3756 | width: 100%; 3757 | height: 100%; 3758 | background: rgba(0,0,0,0.7); 3759 | display: flex; 3760 | justify-content: center; 3761 | align-items: center; 3762 | z-index: 9999; 3763 | backdrop-filter: blur(5px); 3764 | animation: fadeIn 0.3s ease-out; 3765 | `; 3766 | 3767 | const envelopeContainer = document.createElement("div"); 3768 | envelopeContainer.id = "envelope-container"; 3769 | envelopeContainer.style.cssText = ` 3770 | position: relative; 3771 | width: 90%; 3772 | max-width: 650px; 3773 | height: 400px; 3774 | perspective: 1000px; 3775 | `; 3776 | 3777 | const envelope = document.createElement("div"); 3778 | envelope.id = "envelope"; 3779 | envelope.style.cssText = ` 3780 | position: absolute; 3781 | width: 100%; 3782 | height: 100%; 3783 | transform-style: preserve-3d; 3784 | transition: transform 0.6s ease; 3785 | `; 3786 | 3787 | const envelopeBack = document.createElement("div"); 3788 | envelopeBack.id = "envelope-back"; 3789 | envelopeBack.style.cssText = ` 3790 | position: absolute; 3791 | width: 100%; 3792 | height: 100%; 3793 | background: ${COLORS.background}; 3794 | border-radius: 10px; 3795 | box-shadow: 0 15px 35px rgba(0,0,0,0.2); 3796 | backface-visibility: hidden; 3797 | display: flex; 3798 | flex-direction: column; 3799 | align-items: center; 3800 | justify-content: center; 3801 | padding: 30px; 3802 | cursor: pointer; 3803 | transition: all 0.3s; 3804 | `; 3805 | envelopeBack.innerHTML = ` 3806 |
3807 | 致海投用户的一封信 3808 |
3809 |
3810 | 点击开启高效求职之旅 3811 |
3812 |
3813 | © 2025 BOSS海投助手 | Yangshengzhou 版权所有 3814 |
3815 | `; 3816 | 3817 | envelopeBack.addEventListener("click", () => { 3818 | envelope.style.transform = "rotateY(180deg)"; 3819 | setTimeout(() => { 3820 | const content = document.getElementById("letter-content"); 3821 | if (content) { 3822 | content.style.display = "block"; 3823 | content.style.animation = "fadeInUp 0.5s ease-out forwards"; 3824 | } 3825 | }, 300); 3826 | }); 3827 | 3828 | const envelopeFront = document.createElement("div"); 3829 | envelopeFront.id = "envelope-front"; 3830 | envelopeFront.style.cssText = ` 3831 | position: absolute; 3832 | width: 100%; 3833 | height: 100%; 3834 | background: #fff; 3835 | border-radius: 10px; 3836 | box-shadow: 0 15px 35px rgba(0,0,0,0.2); 3837 | transform: rotateY(180deg); 3838 | backface-visibility: hidden; 3839 | display: flex; 3840 | flex-direction: column; 3841 | `; 3842 | 3843 | const titleBar = document.createElement("div"); 3844 | titleBar.style.cssText = ` 3845 | padding: 20px 30px; 3846 | background: linear-gradient(135deg, ${COLORS.primary}, ${COLORS.primaryDark}); 3847 | color: white; 3848 | font-size: clamp(1.2rem, 2.5vw, 1.4rem); 3849 | font-weight: 600; 3850 | border-radius: 10px 10px 0 0; 3851 | display: flex; 3852 | align-items: center; 3853 | `; 3854 | titleBar.innerHTML = `致海投助手用户:`; 3855 | 3856 | const letterContent = document.createElement("div"); 3857 | letterContent.id = "letter-content"; 3858 | letterContent.style.cssText = ` 3859 | flex: 1; 3860 | padding: 25px 30px; 3861 | overflow-y: auto; 3862 | font-size: clamp(0.95rem, 2vw, 1.05rem); 3863 | line-height: 1.8; 3864 | color: ${COLORS.text}; 3865 | 3866 | background-blend-mode: overlay; 3867 | background-color: rgba(255,255,255,0.95); 3868 | display: none; 3869 | `; 3870 | letterContent.innerHTML = ` 3871 |
3872 |

你好,未来的成功人士:

3873 |

  展信如晤。

3874 |

3875 |   我是Yangshengzhou,我曾经和你一样在求职路上反复碰壁。 3876 | 简历石沉大海、面试邀约寥寥、沟通效率低下...于是我做了这个小工具。 3877 |

3878 |

3879 |   现在,我将它分享给你,希望能够帮到你: 3880 |

3881 |
    3882 |
  •   自动沟通页面岗位,一键打招呼
  • 3883 |
  •   AI智能回复HR提问,24小时在线不错过任何机会
  • 3884 |
  •   个性化沟通策略,大幅提升面试邀约率
  • 3885 |
3886 |

3887 |   工具只是辅助,你的能力才是核心竞争力。 3888 | 愿它成为你求职路上的得力助手,助你斩获Offer! 3889 |

3890 |

3891 |   冀以尘雾之微补益山海,荧烛末光增辉日月。 3892 |

3893 |

3894 |   如果插件对你有帮助,请给她点个 Star🌟! 3895 |

3896 |
3897 |
3898 | Yangshengzhou
3899 | 2025年6月于南昌 3900 |
3901 | `; 3902 | 3903 | const buttonArea = document.createElement("div"); 3904 | buttonArea.style.cssText = ` 3905 | padding: 15px 30px; 3906 | display: flex; 3907 | justify-content: center; 3908 | border-top: 1px solid #eee; 3909 | background: ${COLORS.background}; 3910 | border-radius: 0 0 10px 10px; 3911 | `; 3912 | 3913 | const startButton = document.createElement("button"); 3914 | startButton.style.cssText = ` 3915 | background: linear-gradient(135deg, ${COLORS.primary}, ${COLORS.primaryDark}); 3916 | color: white; 3917 | border: none; 3918 | border-radius: 8px; 3919 | padding: 12px 30px; 3920 | font-size: clamp(1rem, 2vw, 1.1rem); 3921 | font-weight: 500; 3922 | cursor: pointer; 3923 | transition: all 0.3s; 3924 | box-shadow: 0 6px 16px rgba(66, 133, 244, 0.3); 3925 | outline: none; 3926 | display: flex; 3927 | align-items: center; 3928 | `; 3929 | startButton.innerHTML = `开始使用`; 3930 | 3931 | startButton.addEventListener("click", () => { 3932 | envelopeContainer.style.animation = "scaleOut 0.3s ease-in forwards"; 3933 | overlay.style.animation = "fadeOut 0.3s ease-in forwards"; 3934 | setTimeout(() => { 3935 | if (overlay.parentNode === document.body) { 3936 | document.body.removeChild(overlay); 3937 | } 3938 | }, 300); 3939 | }); 3940 | 3941 | buttonArea.appendChild(startButton); 3942 | envelopeFront.appendChild(titleBar); 3943 | envelopeFront.appendChild(letterContent); 3944 | envelopeFront.appendChild(buttonArea); 3945 | envelope.appendChild(envelopeBack); 3946 | envelope.appendChild(envelopeFront); 3947 | envelopeContainer.appendChild(envelope); 3948 | overlay.appendChild(envelopeContainer); 3949 | document.body.appendChild(overlay); 3950 | 3951 | const style = document.createElement("style"); 3952 | style.textContent = ` 3953 | @keyframes fadeIn { from { opacity: 0 } to { opacity: 1 } } 3954 | @keyframes fadeOut { from { opacity: 1 } to { opacity: 0 } } 3955 | @keyframes scaleOut { from { transform: scale(1); opacity: 1 } to { transform: scale(.9); opacity: 0 } } 3956 | @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px) } to { opacity: 1; transform: translateY(0) } } 3957 | 3958 | #envelope-back:hover { transform: translateY(-5px); box-shadow: 0 20px 40px rgba(0,0,0,0.25); } 3959 | #envelope-front button:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgba(66, 133, 244, 0.4); } 3960 | #envelope-front button:active { transform: translateY(1px); } 3961 | 3962 | @media (max-width: 480px) { 3963 | #envelope-container { height: 350px; } 3964 | #letter-content { font-size: 0.9rem; padding: 15px; } 3965 | } 3966 | `; 3967 | document.head.appendChild(style); 3968 | }, 3969 | }; 3970 | 3971 | const guide = { 3972 | steps: [ 3973 | { 3974 | target: "div.city-label.active", 3975 | content: 3976 | '👋 海投前,先在BOSS筛选出岗位!\n\n助手会先滚动收集界面上显示的岗位,\n随后依次进行沟通~', 3977 | 3978 | arrowPosition: "bottom", 3979 | defaultPosition: { 3980 | left: "50%", 3981 | top: "20%", 3982 | transform: "translateX(-50%)", 3983 | }, 3984 | }, 3985 | { 3986 | target: 'a[ka="header-jobs"]', 3987 | content: 3988 | '🚀 职位页操作流程:\n\n1️⃣ 扫描职位卡片\n2️⃣ 点击"立即沟通"(需开启“自动打招呼”)\n3️⃣ 留在当前页,继续沟通下一个职位\n\n全程无需手动干预,高效投递!', 3989 | 3990 | arrowPosition: "bottom", 3991 | defaultPosition: { left: "25%", top: "80px" }, 3992 | }, 3993 | { 3994 | target: 'a[ka="header-message"]', 3995 | content: 3996 | '💬 海投建议!\n\n✅ HR与您沟通,HR需要付费给平台\n因此您尽可能先自我介绍以提高效率 \n\n✅ HR查看附件简历,HR也要付费给平台\n所以尽量先发送`图片简历`给HR', 3997 | 3998 | arrowPosition: "left", 3999 | defaultPosition: { right: "150px", top: "100px" }, 4000 | }, 4001 | { 4002 | target: "div.logo", 4003 | content: 4004 | '🤖 您需要打开两个浏览器窗口:\n\n左侧窗口自动打招呼发起沟通\n右侧发送自我介绍和图片简历\n\n您只需专注于挑选offer!', 4005 | 4006 | arrowPosition: "right", 4007 | defaultPosition: { left: "200px", top: "20px" }, 4008 | }, 4009 | { 4010 | target: "div.logo", 4011 | content: 4012 | '❗ 特别注意:\n\n1. BOSS直聘每日打招呼上限为150次\n2. 聊天页仅处理最上方的最新对话\n3. 打招呼后对方会显示在聊天页\n4. 投递操作过于频繁有封号风险!', 4013 | 4014 | arrowPosition: "bottom", 4015 | defaultPosition: { left: "50px", top: "80px" }, 4016 | }, 4017 | ], 4018 | currentStep: 0, 4019 | guideElement: null, 4020 | overlay: null, 4021 | highlightElements: [], 4022 | 4023 | showGuideToUser() { 4024 | this.overlay = document.createElement("div"); 4025 | this.overlay.id = "guide-overlay"; 4026 | this.overlay.style.cssText = ` 4027 | position: fixed; 4028 | top: 0; 4029 | left: 0; 4030 | width: 100%; 4031 | height: 100%; 4032 | background: rgba(0, 0, 0, 0.5); 4033 | backdrop-filter: blur(2px); 4034 | z-index: 99997; 4035 | pointer-events: none; 4036 | opacity: 0; 4037 | transition: opacity 0.3s ease; 4038 | `; 4039 | document.body.appendChild(this.overlay); 4040 | 4041 | this.guideElement = document.createElement("div"); 4042 | this.guideElement.id = "guide-tooltip"; 4043 | this.guideElement.style.cssText = ` 4044 | position: fixed; 4045 | z-index: 99999; 4046 | width: 320px; 4047 | background: white; 4048 | border-radius: 12px; 4049 | box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04); 4050 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; 4051 | overflow: hidden; 4052 | opacity: 0; 4053 | transform: translateY(10px); 4054 | transition: opacity 0.3s ease, transform 0.3s ease; 4055 | `; 4056 | document.body.appendChild(this.guideElement); 4057 | 4058 | setTimeout(() => { 4059 | this.overlay.style.opacity = "1"; 4060 | 4061 | setTimeout(() => { 4062 | this.showStep(0); 4063 | }, 300); 4064 | }, 100); 4065 | }, 4066 | 4067 | showStep(stepIndex) { 4068 | const step = this.steps[stepIndex]; 4069 | if (!step) return; 4070 | 4071 | this.clearHighlights(); 4072 | const target = document.querySelector(step.target); 4073 | 4074 | if (target) { 4075 | const rect = target.getBoundingClientRect(); 4076 | const highlight = document.createElement("div"); 4077 | highlight.className = "guide-highlight"; 4078 | highlight.style.cssText = ` 4079 | position: fixed; 4080 | top: ${rect.top}px; 4081 | left: ${rect.left}px; 4082 | width: ${rect.width}px; 4083 | height: ${rect.height}px; 4084 | background: ${step.highlightColor || "#4285f4"}; 4085 | opacity: 0.2; 4086 | border-radius: 4px; 4087 | z-index: 99998; 4088 | box-shadow: 0 0 0 4px ${step.highlightColor || "#4285f4"}; 4089 | animation: guide-pulse 2s infinite; 4090 | `; 4091 | document.body.appendChild(highlight); 4092 | this.highlightElements.push(highlight); 4093 | 4094 | this.setGuidePositionFromTarget(step, rect); 4095 | } else { 4096 | console.warn("引导目标元素未找到,使用默认位置:", step.target); 4097 | 4098 | this.setGuidePositionFromDefault(step); 4099 | } 4100 | 4101 | let buttonsHtml = ""; 4102 | 4103 | if (stepIndex === this.steps.length - 1) { 4104 | buttonsHtml = ` 4105 |
4106 | 4111 |
4112 | `; 4113 | } else { 4114 | buttonsHtml = ` 4115 |
4116 | 4117 | 4120 |
4121 | `; 4122 | } 4123 | 4124 | this.guideElement.innerHTML = ` 4125 |
4128 |
海投助手引导
4129 |
步骤 ${ 4130 | stepIndex + 1 4131 | }/${this.steps.length}
4132 |
4133 |
4134 |
${ 4135 | step.content 4136 | }
4137 |
4138 | ${buttonsHtml} 4139 | `; 4140 | 4141 | if (stepIndex === this.steps.length - 1) { 4142 | document 4143 | .getElementById("guide-finish-btn") 4144 | .addEventListener("click", () => this.endGuide(true)); 4145 | } else { 4146 | document 4147 | .getElementById("guide-next-btn") 4148 | .addEventListener("click", () => this.nextStep()); 4149 | document 4150 | .getElementById("guide-skip-btn") 4151 | .addEventListener("click", () => this.endGuide()); 4152 | } 4153 | 4154 | if (stepIndex === this.steps.length - 1) { 4155 | const finishBtn = document.getElementById("guide-finish-btn"); 4156 | finishBtn.addEventListener("mouseenter", () => { 4157 | finishBtn.style.background = this.darkenColor( 4158 | step.highlightColor || "#4285f4", 4159 | 15 4160 | ); 4161 | finishBtn.style.boxShadow = 4162 | "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)"; 4163 | }); 4164 | finishBtn.addEventListener("mouseleave", () => { 4165 | finishBtn.style.background = step.highlightColor || "#4285f4"; 4166 | finishBtn.style.boxShadow = 4167 | "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)"; 4168 | }); 4169 | } else { 4170 | const nextBtn = document.getElementById("guide-next-btn"); 4171 | const skipBtn = document.getElementById("guide-skip-btn"); 4172 | 4173 | nextBtn.addEventListener("mouseenter", () => { 4174 | nextBtn.style.background = this.darkenColor( 4175 | step.highlightColor || "#4285f4", 4176 | 15 4177 | ); 4178 | nextBtn.style.boxShadow = 4179 | "0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06)"; 4180 | }); 4181 | nextBtn.addEventListener("mouseleave", () => { 4182 | nextBtn.style.background = step.highlightColor || "#4285f4"; 4183 | nextBtn.style.boxShadow = 4184 | "0 1px 3px 0 rgba(0, 0, 0, 0.1), 0 1px 2px 0 rgba(0, 0, 0, 0.06)"; 4185 | }); 4186 | 4187 | skipBtn.addEventListener("mouseenter", () => { 4188 | skipBtn.style.background = "#f3f4f6"; 4189 | }); 4190 | skipBtn.addEventListener("mouseleave", () => { 4191 | skipBtn.style.background = "white"; 4192 | }); 4193 | } 4194 | 4195 | this.guideElement.style.opacity = "1"; 4196 | this.guideElement.style.transform = "translateY(0)"; 4197 | }, 4198 | 4199 | setGuidePositionFromTarget(step, rect) { 4200 | let left, top; 4201 | const guideWidth = 320; 4202 | const guideHeight = 240; 4203 | 4204 | switch (step.arrowPosition) { 4205 | case "top": 4206 | left = rect.left + rect.width / 2 - guideWidth / 2; 4207 | top = rect.top - guideHeight - 20; 4208 | break; 4209 | case "bottom": 4210 | left = rect.left + rect.width / 2 - guideWidth / 2; 4211 | top = rect.bottom + 20; 4212 | break; 4213 | case "left": 4214 | left = rect.left - guideWidth - 20; 4215 | top = rect.top + rect.height / 2 - guideHeight / 2; 4216 | break; 4217 | case "right": 4218 | left = rect.right + 20; 4219 | top = rect.top + rect.height / 2 - guideHeight / 2; 4220 | break; 4221 | default: 4222 | left = rect.right + 20; 4223 | top = rect.top; 4224 | } 4225 | 4226 | left = Math.max(10, Math.min(left, window.innerWidth - guideWidth - 10)); 4227 | top = Math.max(10, Math.min(top, window.innerHeight - guideHeight - 10)); 4228 | 4229 | this.guideElement.style.left = `${left}px`; 4230 | this.guideElement.style.top = `${top}px`; 4231 | this.guideElement.style.transform = "translateY(0)"; 4232 | }, 4233 | 4234 | setGuidePositionFromDefault(step) { 4235 | const position = step.defaultPosition || { 4236 | left: "50%", 4237 | top: "50%", 4238 | transform: "translate(-50%, -50%)", 4239 | }; 4240 | 4241 | Object.assign(this.guideElement.style, { 4242 | left: position.left, 4243 | top: position.top, 4244 | right: position.right || "auto", 4245 | bottom: position.bottom || "auto", 4246 | transform: position.transform || "none", 4247 | }); 4248 | }, 4249 | 4250 | nextStep() { 4251 | const currentStep = this.steps[this.currentStep]; 4252 | if (currentStep) { 4253 | const target = document.querySelector(currentStep.target); 4254 | if (target) { 4255 | target.removeEventListener("click", this.nextStep); 4256 | } 4257 | } 4258 | 4259 | this.currentStep++; 4260 | if (this.currentStep < this.steps.length) { 4261 | this.guideElement.style.opacity = "0"; 4262 | this.guideElement.style.transform = "translateY(10px)"; 4263 | 4264 | setTimeout(() => { 4265 | this.showStep(this.currentStep); 4266 | }, 300); 4267 | } else { 4268 | } 4269 | }, 4270 | 4271 | clearHighlights() { 4272 | this.highlightElements.forEach((el) => el.remove()); 4273 | this.highlightElements = []; 4274 | }, 4275 | 4276 | endGuide(isCompleted = false) { 4277 | this.clearHighlights(); 4278 | 4279 | this.guideElement.style.opacity = "0"; 4280 | this.guideElement.style.transform = "translateY(10px)"; 4281 | this.overlay.style.opacity = "0"; 4282 | 4283 | setTimeout(() => { 4284 | if (this.overlay && this.overlay.parentNode) { 4285 | this.overlay.parentNode.removeChild(this.overlay); 4286 | } 4287 | if (this.guideElement && this.guideElement.parentNode) { 4288 | this.guideElement.parentNode.removeChild(this.guideElement); 4289 | } 4290 | 4291 | if (isCompleted && this.chatUrl) { 4292 | window.open(this.chatUrl, "_blank"); 4293 | } 4294 | }, 300); 4295 | 4296 | document.dispatchEvent(new Event("guideEnd")); 4297 | }, 4298 | 4299 | darkenColor(color, percent) { 4300 | let R = parseInt(color.substring(1, 3), 16); 4301 | let G = parseInt(color.substring(3, 5), 16); 4302 | let B = parseInt(color.substring(5, 7), 16); 4303 | 4304 | R = parseInt((R * (100 - percent)) / 100); 4305 | G = parseInt((G * (100 - percent)) / 100); 4306 | B = parseInt((B * (100 - percent)) / 100); 4307 | 4308 | R = R < 255 ? R : 255; 4309 | G = G < 255 ? G : 255; 4310 | B = B < 255 ? B : 255; 4311 | 4312 | R = Math.round(R); 4313 | G = Math.round(G); 4314 | B = Math.round(B); 4315 | 4316 | const RR = 4317 | R.toString(16).length === 1 ? "0" + R.toString(16) : R.toString(16); 4318 | const GG = 4319 | G.toString(16).length === 1 ? "0" + G.toString(16) : G.toString(16); 4320 | const BB = 4321 | B.toString(16).length === 1 ? "0" + B.toString(16) : B.toString(16); 4322 | 4323 | return `#${RR}${GG}${BB}`; 4324 | }, 4325 | }; 4326 | 4327 | const style = document.createElement("style"); 4328 | style.textContent = ` 4329 | @keyframes guide-pulse { 4330 | 0% { transform: scale(1); box-shadow: 0 0 0 0 rgba(66, 133, 244, 0.4); } 4331 | 70% { transform: scale(1); box-shadow: 0 0 0 10px rgba(66, 133, 244, 0); } 4332 | 100% { transform: scale(1); box-shadow: 0 0 0 0 rgba(66, 133, 244, 0); } 4333 | } 4334 | 4335 | .guide-content .highlight { 4336 | font-weight: 700; 4337 | color: #1a73e8; 4338 | } 4339 | 4340 | .guide-content .warning { 4341 | font-weight: 700; 4342 | color: #d93025; 4343 | } 4344 | `; 4345 | document.head.appendChild(style); 4346 | 4347 | const STORAGE = { 4348 | LETTER: "letterLastShown", 4349 | GUIDE: "shouldShowGuide", 4350 | AI_COUNT: "aiReplyCount", 4351 | AI_DATE: "lastAiDate", 4352 | }; 4353 | 4354 | function getToday() { 4355 | return new Date().toISOString().split("T")[0]; 4356 | } 4357 | 4358 | function init() { 4359 | try { 4360 | const midnight = new Date(); 4361 | midnight.setDate(midnight.getDate() + 1); 4362 | midnight.setHours(0, 0, 0, 0); 4363 | setTimeout(() => { 4364 | localStorage.removeItem(STORAGE.AI_COUNT); 4365 | localStorage.removeItem(STORAGE.AI_DATE); 4366 | localStorage.removeItem(STORAGE.LETTER); 4367 | }, midnight - Date.now()); 4368 | UI.init(); 4369 | document.body.style.position = "relative"; 4370 | const today = getToday(); 4371 | if (location.pathname.includes("/jobs")) { 4372 | if (localStorage.getItem(STORAGE.LETTER) !== today) { 4373 | letter.showLetterToUser(); 4374 | localStorage.setItem(STORAGE.LETTER, today); 4375 | } else if (localStorage.getItem(STORAGE.GUIDE) !== "true") { 4376 | guide.showGuideToUser(); 4377 | localStorage.setItem(STORAGE.GUIDE, "true"); 4378 | Core.delay(800); 4379 | window.open( 4380 | "https://www.zhipin.com/web/geek/notify-set?ka=notify-set", 4381 | "_blank" 4382 | ); 4383 | } 4384 | Core.log("欢迎使用海投助手,我将自动投递岗位!"); 4385 | } else if (location.pathname.includes("/chat")) { 4386 | Core.log("欢迎使用海投助手,我将自动发送简历!"); 4387 | } else if (location.pathname.includes("/notify-set")) { 4388 | Core.log("请将常用语换为自我介绍来引起HR的注意!"); 4389 | 4390 | const targetSelector = "h3.normal.title"; 4391 | 4392 | const observer = new MutationObserver((mutations, obs) => { 4393 | const targetElement = document.querySelector(targetSelector); 4394 | if (targetElement) { 4395 | targetElement.textContent = 4396 | "把常用语换为自我介绍,并设图片简历; 招呼语功能必须启用。"; 4397 | obs.disconnect(); 4398 | } 4399 | }); 4400 | 4401 | observer.observe(document.body, { 4402 | childList: true, 4403 | subtree: true, 4404 | }); 4405 | } else { 4406 | Core.log("当前页面暂不支持,请移步至职位页面!"); 4407 | } 4408 | } catch (error) { 4409 | console.error("初始化失败:", error); 4410 | if (UI.notify) UI.notify("初始化失败", "error"); 4411 | } 4412 | } 4413 | 4414 | window.addEventListener("load", init); 4415 | })(); 4416 | --------------------------------------------------------------------------------