Metadata-Version: 2.5
Name: recruiter-msg
Version: 0.1.2
Summary: LinkedIn recruiter message automation toolkit
Author-email: Chandler <275737875@qq.com>
License-Expression: MIT
License-File: LICENSE
Keywords: automation,linkedin,messaging,recruiter
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.9
Requires-Dist: beautifulsoup4>=4.9.0
Requires-Dist: diskcache>=5.0.0
Requires-Dist: openai>=1.0.0
Requires-Dist: openpyxl>=3.0.0
Requires-Dist: pandas>=1.3.0
Requires-Dist: python-dotenv>=1.0.0
Requires-Dist: pyyaml>=6.0.0
Requires-Dist: selenium>=4.0.0
Requires-Dist: tenacity>=8.0.0
Requires-Dist: typer>=0.9.0
Provides-Extra: dev
Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Description-Content-Type: text/markdown

# recruiter-msg

LinkedIn 招聘者消息自动化工具包 —— 基于 Selenium 的 LinkedIn Recruiter 消息批量发送、候选人筛选与去重管理工具。

---

## 功能特性

- **批量消息发送**：从 Excel 读取候选人列表，自动导航至 LinkedIn Recruiter 页面并发送定制消息
- **智能去重**：基于 DiskCache + JSON 断点的双重去重机制，避免重复发送
- **候选人筛选**：支持基于关键词和 LLM 的姓名/职位评估，自动过滤非技术岗位
- **URN 缓存**：自动缓存公开 URL 与 Recruiter URN 的映射关系，加速后续访问
- **简历提取**：从 LinkedIn 页面自动提取候选人资料文本并保存
- **LLM 集成**：支持 OpenAI 兼容 API（DeepSeek、Ollama 等）进行智能评估
- **模板变量**：消息正文支持 `{name}`、`{title}`、`{email}`、`{linkedin_url}` 动态替换
- **CLI 命令行**：提供完整的 Typer CLI 工具，开箱即用
- **配置化管理**：支持 YAML 配置文件，字段可选省略，内置默认值，零配置启动

---

## 安装

### 从 PyPI 安装

```bash
pip install recruiter-msg
```

---

## 快速开始

### 第一步：初始化配置

```bash
recruiter-msg init
```

此命令会在当前目录生成 `.env` 和 `config.yaml` 两个配置文件：

**`.env` 文件**（LLM 配置，可选）：

```ini
# LLM 配置
LLM_API_KEY=your-api-key-here
LLM_BASE_URL=http://api.openai.com/v1
LLM_MODEL=deepseek-v3.1-terminus-chat
LLM_TIMEOUT=120
LLM_MAX_RETRIES=3
```

> 如果不使用 LLM 评估功能，可跳过 `.env` 配置。

**`config.yaml` 文件**（LinkedIn 自动化配置）：

```yaml
# config.yaml — LinkedIn 自动化配置
# 字段说明：
#   - status_keywords: 消息状态关键词（如 Accepted、Deleted），用于判断是否已发送
#   - subject_keywords: 消息主题关键词（如 Agent），用于判断是否已发送
#   - recruiter: 招聘者标识，匹配消息发送者姓名
#   - blacklist: 黑名单（可选）
#   - linkedin_list: LinkedIn 列表（可选）

status_keywords:
  - Accepted
  - Deleted

subject_keywords:
  - Agent

recruiter: chandler

blacklist: []
linkedin_list: []
```

**字段默认值**：

| 字段 | 默认值 | 说明 |
|------|--------|------|
| `status_keywords` | `["Accepted", "Deleted"]` | 消息状态关键词列表 |
| `subject_keywords` | `[]` | 消息主题关键词列表 |
| `recruiter` | `"chandler"` | 招聘者姓名 |
| `blacklist` | `[]` | 黑名单 URL 列表 |
| `linkedin_list` | `[]` | LinkedIn 列表 |

> 所有字段均为可选，省略时使用默认值。`config.yaml` 文件本身也是可选的，不存在时自动使用代码内置默认值。

### 第二步：准备 Cookie 文件

本工具需要 LinkedIn Recruiter 的登录 Cookie 来访问招聘者页面：

1. 用浏览器登录 [LinkedIn Recruiter](https://www.linkedin.com/talent/home)
2. 安装 Cookie 导出浏览器插件（推荐 [EditThisCookie](https://www.editthiscookie.com/) 或 [Cookie-Editor](https://cookie-editor.com/)）
3. 导出 Cookie 为 JSON 格式
4. 保存为 `cookie.json` 到当前目录（或通过 `--cookie-file` 指定路径）

### 第三步：准备候选人 Excel 文件

创建一个 Excel 文件（`.xlsx`），包含以下列：

| 列名              | 说明                                                         |
| ----------------- | ------------------------------------------------------------ |
| `url`             | LinkedIn 个人资料 URL（公开链接或 Recruiter 链接）           |
| `tag`             | 标签，用于过滤（如 `"agent"`）                               |
| `recommend_title` | 推荐职位标题，同时用作消息主题                               |
| `email_content`   | 消息正文内容，支持模板变量替换                               |

**模板变量**：在 `email_content` 中可使用以下变量，发送时自动替换：

- `{name}` — 候选人姓名（从 LinkedIn 页面解析）
- `{title}` — 候选人当前职位
- `{email}` — 候选人邮箱（需配置邮箱数据源）
- `{linkedin_url}` — 候选人 LinkedIn URL

**示例 Excel 数据**：

| url | tag | recommend_title | email_content |
|-----|-----|----------------|---------------|
| https://linkedin.com/in/zhangsan | agent | 高级Java工程师 | 你好 {name}，我们对你的背景很感兴趣... |
| https://linkedin.com/in/lisi | agent | AI算法工程师 | Hi {name}，有一个 {title} 的机会想和你聊聊... |

### 第四步：运行自动化

```bash
# 基本运行（discover 模式，使用 URN 缓存）
recruiter-msg run --config config.yaml --excel ./candidates.xlsx --tag agent --mode discover

# 公开链接模式（首次运行，自动发现 URN 并缓存）
recruiter-msg run --config config.yaml --excel ./candidates.xlsx --tag agent --mode public-url

# Recruiter 链接模式（Excel 中直接提供 talent/profile 链接）
recruiter-msg run --config config.yaml --excel ./candidates.xlsx --tag agent --mode recruiter-url

# 启用 LLM 评估（智能筛选候选人）
recruiter-msg run --config config.yaml --excel ./candidates.xlsx --llm --mode discover

# 指定切片范围（只处理第 5-10 位候选人）
recruiter-msg run --config config.yaml --excel ./candidates.xlsx --start 4 --end 10

# 自定义延时（避免触发频率限制）
recruiter-msg run --config config.yaml --excel ./candidates.xlsx --delay-min 2.0 --delay-max 5.0

# 无头模式（不显示浏览器窗口）
recruiter-msg run --config config.yaml --excel ./candidates.xlsx --headless

# 详细日志输出
recruiter-msg run --config config.yaml --excel ./candidates.xlsx -v

# 不指定 --config，自动查找 ./config.yaml，不存在则使用默认值
recruiter-msg run --excel ./candidates.xlsx --tag agent
```

### 处理模式说明

| 模式             | 适用场景 | 说明 |
|------------------|----------|------|
| `discover`       | 已有 URN 缓存 | Excel 提供公开链接，先查 URN 缓存，有则直接跳转招聘者页面，无缓存则失败 |
| `public-url`     | 首次运行 | Excel 提供公开链接，从公开页面导航并自动发现招聘者链接，缓存 URN |
| `recruiter-url`  | 仅有招聘者链接 | Excel 提供招聘者链接，直接访问并回填 URN 缓存 |

### 其他命令

```bash
# 列出 Excel 中的候选人
recruiter-msg list-candidates --excel ./candidates.xlsx --tag agent --limit 20

# 检查某个标识符是否已发送
recruiter-msg check-duplicate "https://linkedin.com/in/zhangsan"

# 初始化配置文件（生成 .env 和 config.yaml，使用 --force 覆盖已有文件）
recruiter-msg init --force
```

---

## 项目架构

### 目录结构

```
recruiter-msg/
├── pyproject.toml              # 项目构建配置与依赖声明
├── config.yaml                 # YAML 配置文件（可选）
├── README.md                   # 项目文档
├── LICENSE                     # MIT 许可证
├── src/recruiter_msg/
│   ├── __init__.py             # 包入口，导出所有公共 API
│   ├── models.py               # 数据模型定义
│   ├── interfaces.py           # 抽象接口（策略模式）
│   ├── exceptions.py           # 自定义异常
│   ├── checker.py              # 去重检查器实现
│   ├── datasource.py           # 候选人数据源实现
│   ├── parser.py               # LinkedIn 页面解析器
│   ├── navigator.py            # LinkedIn 页面导航器
│   ├── sender.py               # LinkedIn 消息发送器
│   ├── evaluator.py            # 评估器实现
│   ├── strategy.py             # 消息策略实现
│   ├── orchestrator.py         # 核心编排器与工厂函数
│   ├── resume.py               # 简历文本提取
│   ├── llm.py                  # LLM 客户端
│   └── cli.py                  # CLI 命令行入口
└── tests/                      # 测试目录
```

### 核心设计模式

本项目采用 **策略模式（Strategy Pattern）** 设计，所有核心功能通过抽象接口定义，具体实现可替换：

```
┌─────────────────────────────────────────────────────────────┐
│                    CLI (cli.py / Typer)                      │
│                  recruiter-msg run / list / ...              │
└──────────────────────────┬──────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────────┐
│              Orchestrator (orchestrator.py)                  │
│           LinkedInAutomationOrchestrator                     │
│                                                              │
│  process_candidate() / process_candidate_urn()              │
│  process_candidate_direct_urn()                              │
│                                                              │
│  编排完整流程：导航 → 解析 → 评估 → 去重 → 生成消息 → 发送    │
└──┬──────┬──────┬──────┬──────┬──────┬──────┬──────┬─────────┘
   │      │      │      │      │      │      │      │
   ▼      ▼      ▼      ▼      ▼      ▼      ▼      ▼
┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐┌─────┐
│Nav  ││Parse││Name ││Title││Dedup││Msg  ││Send ││Data │
│     ││     ││Eval ││Eval ││Chk  ││Strat││     ││Src  │
└──┬──┘└──┬──┘└──┬──┘└──┬──┘└──┬──┘└──┬──┘└──┬──┘└──┬──┘
   │       │       │       │       │       │       │       │
   ▼       ▼       ▼       ▼       ▼       ▼       ▼       ▼
 接口层 (interfaces.py) — 9 个抽象基类
```

### 接口与实现对照表

| 抽象接口                | 实现类                          | 职责                         |
| ----------------------- | ------------------------------- | ---------------------------- |
| `ProfileParser`         | `LinkedInProfileParser`         | 解析 LinkedIn 个人资料页面   |
| `MessageStrategy`       | `ExcelMessageStrategy`          | 基于模板生成消息内容         |
| `DuplicateChecker`      | `DiskCacheDuplicateChecker`     | 基于 DiskCache 的去重        |
|                         | `JsonCheckpointDuplicateChecker`| 基于 JSON 文件的断点去重     |
|                         | `CompositeDuplicateChecker`     | 组合多个去重检查器           |
| `Navigator`             | `LinkedInNavigator`             | LinkedIn 页面导航与交互      |
| `MessageSender`         | `LinkedInMessageSender`         | 消息输入与发送               |
| `NameEvaluator`         | `LLMNameEvaluator`              | 基于 LLM 的姓名分类评估      |
| `TitleEvaluator`        | `KeywordTitleEvaluator`         | 基于关键词的职位筛选         |
|                         | `LLMTitleEvaluator`             | 基于 LLM 的技术岗位识别      |
| `CandidateDataSource`   | `ExcelCandidateDataSource`      | 从 Excel 加载候选人数据      |
| `EmailDataSource`       | （需用户自行实现）              | 邮箱数据获取                 |

### 数据模型

```
Config                    全局配置（状态关键词、主题关键词、招聘者标识、黑名单等）
                          - 支持从 YAML 文件加载：Config.from_yaml("config.yaml")
                          - 所有字段均有默认值，可省略
Candidate                 候选人完整数据（URL、ID、URN、消息标题/正文、标签）
CandidateProfile          从页面解析的候选人资料（姓名、职位、LinkedIn URL）
MessageContent            消息内容（标题 + 正文）
MessageThread             消息线程（发件人、主题、状态、日期、预览）
```

### 处理流程

以 `discover` 模式为例，单个候选人的完整处理流程：

```
1. 查询 URN 缓存（公开 URL → Recruiter URN）
2. 导航至 Recruiter 个人资料页
3. 解析候选人资料（姓名、职位）
4. 评估姓名（可选，LLM 判断国籍过滤）
5. 评估职位（关键词过滤或 LLM 判断技术岗）
6. 保存简历文本（可选）
7. 检查消息历史（避免重复发送）
8. 检查缓存去重
9. 检查消息按钮可用性
10. 打开消息对话框
11. 生成消息内容（模板变量替换）
12. 发送消息
13. 标记为已发送（写入去重缓存 + 断点文件）
```

---

## 编程接口使用

除了 CLI 命令行，也可以在 Python 代码中直接调用：

```python
from recruiter_msg import (
    Config,
    LinkedInAutomationOrchestrator,
    create_default_orchestrator,
    LLMClient,
)

# 1. 创建配置（方式一：从 YAML 文件加载）
config = Config.from_yaml("./config.yaml")

# 1. 创建配置（方式二：直接构造）
config = Config(
    status_keywords=["Accepted", "Deleted"],
    subject_keywords=["Agent"],
    recruiter="chandler",
    blacklist=[],
    linkedin_list=[],
)

# 2. 创建编排器和数据源（需要先初始化 Selenium driver）
orchestrator, data_source = create_default_orchestrator(
    driver=driver,
    config=config,
    excel_path="./candidates.xlsx",
    tag_filter="agent",
    use_llm=True,               # 启用 LLM 评估
    urn_cache_dir="./cache_dir", # URN 缓存目录
    resume_dir="./resume",       # 简历保存目录
)

# 3. 加载候选人
candidates = data_source.fetch_candidates(tag_filter="agent")

# 4. 逐个处理
for candidate in candidates:
    result = orchestrator.process_candidate_urn(candidate)
    print(f"{'成功' if result else '失败'}: {candidate.linkedin_url}")
```

### 自定义实现

所有核心组件均可替换，只需实现对应的抽象接口：

```python
from recruiter_msg import DuplicateChecker, MessageStrategy, Candidate, CandidateProfile, MessageContent

# 自定义去重检查器
class RedisDuplicateChecker(DuplicateChecker):
    def is_duplicate(self, identifier: str, content_key: str) -> bool:
        # 使用 Redis 实现
        ...

    def mark_as_sent(self, identifier: str, content_key: str) -> None:
        ...

# 自定义消息策略
class AIMessageStrategy(MessageStrategy):
    def generate_content(self, candidate: Candidate, profile: CandidateProfile) -> MessageContent:
        # 使用 LLM 动态生成消息
        ...

# 注入到编排器
orchestrator = LinkedInAutomationOrchestrator(
    driver=driver,
    duplicate_checker=RedisDuplicateChecker(),
    message_strategy=AIMessageStrategy(),
    ...
)
```

---

## LLM 客户端配置

`LLMClient` 支持任何 OpenAI 兼容的 API 服务。

### .env 文件加载优先级

`LLMClient` 初始化时自动加载 `.env` 文件，优先级如下：

1. **显式路径**：通过 `env_file` 参数指定（最高优先级）
2. **当前目录**：`./.env`
3. **家目录**：`~/.env`（最低优先级，支持跨项目复用）

> 若当前目录和家目录均无 `.env` 文件，抛出 `EnvNotFoundError`；若 `.env` 存在但缺少 `LLM_API_KEY`，抛出 `ValueError`。

### .env 文件内容

```ini
LLM_API_KEY=your-api-key-here
LLM_BASE_URL=https://api.deepseek.com/v1
LLM_MODEL=deepseek-chat
LLM_TIMEOUT=120
LLM_MAX_RETRIES=3
```

### 使用示例

```python
from recruiter_msg import LLMClient

# 方式一：自动加载 .env（推荐）
# 优先级：当前目录 > 家目录
client = LLMClient()

# 方式二：显式指定 .env 路径
client = LLMClient(env_file="/custom/path/.env")

# 方式三：直接传参（覆盖 .env 配置）
client = LLMClient(
    api_key="sk-xxx",
    base_url="https://api.deepseek.com/v1",
    model="deepseek-chat",
    timeout=120,
    max_retries=3,
)

# 方式四：使用本地 Ollama
client = LLMClient(
    api_key="ollama",
    base_url="http://localhost:11434/v1",
    model="qwen2.5:7b",
)

# 调用
response = client.chat("你是助手", "你好")
result = client.chat_json("你是分类专家", "判断以下职位是否为技术岗：Java工程师")
```

### 异常说明

| 异常 | 触发条件 | 处理建议 |
|------|---------|---------|
| `EnvNotFoundError` | 当前目录和家目录均无 `.env` | 按异常消息中的教程创建 `.env` |
| `ValueError` | `.env` 存在但缺少 `LLM_API_KEY` | 在 `.env` 中补充 `LLM_API_KEY` 配置 |

---

## 常见问题

### Q: Cookie 过期怎么办？

重新导出 LinkedIn Cookie 并替换 `cookie.json` 文件即可。LinkedIn Cookie 通常有效期为数天至数周。

### Q: 如何避免被 LinkedIn 限制？

- 使用 `--delay-min` 和 `--delay-max` 设置合理的操作间隔（建议 2-5 秒）
- 使用 `--start` 和 `--end` 分批处理候选人
- 避免短时间内大量操作

### Q: URN 缓存有什么用？

LinkedIn Recruiter 页面使用 URN（如 `ACoAABxxx`）标识候选人，而 Excel 中通常只有公开链接。URN 缓存将公开 URL 映射到 Recruiter URN，避免每次都需要从公开页面重新发现 Recruiter 链接，大幅提升处理速度。

---

## 版本更新日志

### v1.0.1 (2026-08-26)

**更新**：`.env` 文件加载优先级优化

#### 新增功能
- **多路径 `.env` 加载**：支持当前目录 → 家目录回退，家目录 `.env` 可跨项目复用
- **显式路径指定**：`LLMClient` 新增 `env_file` 参数，支持自定义 `.env` 路径
- **新增异常类**：`EnvNotFoundError`，未找到 `.env` 时抛出含完整配置教程的异常

#### 破坏性变更
- **移除默认密钥**：`LLM_API_KEY` 不再使用内置默认值 `"sk-1234"`，未配置时抛出 `ValueError`
- **强制配置**：未找到 `.env` 文件时抛出 `EnvNotFoundError`，不再静默使用假密钥

#### 异常说明
| 异常 | 触发条件 |
|------|---------|
| `EnvNotFoundError` | 当前目录和家目录均无 `.env` 文件 |
| `ValueError` | `.env` 存在但缺少 `LLM_API_KEY` 配置 |

#### 加载优先级
1. 显式路径（`env_file` 参数）
2. 当前目录（`./.env`）
3. 家目录（`~/.env`）

### v1.0.0 (2026-08-24)

**重大更新**：配置化改造与代码清理

#### 新增功能
- **YAML 配置支持**：新增 `config.yaml` 配置文件，支持字段默认值和可选省略
- **零配置启动**：`--config` 参数可选，文件不存在时自动回退到代码内置默认值
- **init 命令增强**：`init` 命令同时生成 `.env` 和 `config.yaml`，并提供完整的使用和配置教程

#### 配置变更
| 字段 | 默认值 | 说明 |
|------|--------|------|
| `status_keywords` | `["Accepted", "Deleted"]` | 消息状态关键词列表 |
| `subject_keywords` | `[]` | 消息主题关键词列表 |
| `recruiter` | `"chandler"` | 招聘者姓名 |
| `blacklist` | `[]` | 黑名单 URL 列表 |
| `linkedin_list` | `[]` | LinkedIn 列表 |

#### CLI 变更
- **新增**：`run` 命令 `--config` 参数（默认 `./config.yaml`）
- **删除**：`run` 命令 `--recruiter` / `-r` 参数（改用 `config.yaml` 配置）

#### 代码优化
- **修复**：关键词匹配逻辑改为大小写不敏感（`k.lower()` 转换）
- **清理**：移除所有调试输出和注释代码，保持代码整洁

#### 依赖变更
- 新增 `pyyaml>=6.0.0` 依赖

#### 向后兼容性
- 原有 CLI 调用方式 `recruiter-msg run --recruiter monica` 不再支持
- 新 CLI 调用方式 `recruiter-msg run --config config.yaml`
- 若 `config.yaml` 省略 `recruiter` 字段，仍使用默认值 `"chandler"`

---

## 免责声明

**本工具仅供学习和研究目的使用。**

1. **服务条款**：本工具的自动化操作可能违反 LinkedIn 的服务条款（Terms of Service）和用户协议。使用本工具即表示你知悉并自行承担因违反服务条款而导致的一切后果，包括但不限于账号被限制、封禁或法律追诉。

2. **合规使用**：使用者应确保其使用方式符合所在地区的法律法规，包括但不限于数据隐私法（如 GDPR、CCPA 等）、反垃圾信息法以及劳动就业相关法规。

3. **无担保**：本工具按"原样"（AS IS）提供，不作任何明示或暗示的担保，包括但不限于适销性、特定用途适用性和非侵权性。作者不对使用本工具所产生的任何直接、间接、附带、特殊或后果性损害承担责任。

4. **风险自担**：使用者应当充分理解本工具的功能和潜在风险，自行评估并承担所有使用风险。作者不对因使用本工具而导致的任何损失、数据泄露、账号问题或其他不良后果承担任何责任。

5. **非商业用途**：本工具不鼓励也不支持任何形式的商业滥用、大规模自动化骚扰或垃圾信息发送行为。

6. **修改与终止**：作者保留随时修改、更新或终止本工具的权利，无需事先通知。

**使用本工具即表示你已阅读、理解并同意以上免责声明的所有条款。如果你不同意，请勿使用本工具。**
