Metadata-Version: 2.4
Name: cold-msg
Version: 0.1.3
Summary: 招聘冷邮件智能生成工具 - 基于候选人简历文本自动生成 Cold Email
Author-email: Chandler <275737875@qq.com>
License: MIT
Keywords: cold-email,recruiting,llm,ai,hr
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.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: llmdog>=0.1.0
Requires-Dist: flask>=3.0.0
Requires-Dist: typer>=0.9.0
Requires-Dist: rich>=13.0.0
Requires-Dist: pyyaml>=6.0
Requires-Dist: python-dotenv>=1.0.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0; extra == "dev"
Requires-Dist: ruff>=0.1.0; extra == "dev"
Requires-Dist: mypy>=1.0; extra == "dev"

# ColdMsg - 招聘冷邮件智能生成工具

基于候选人简历文本，利用大语言模型（LLM）自动匹配职位并生成个性化 Cold Email 的工具。支持命令行和 Web 界面两种使用方式。

## 功能特性

- **智能匹配**：根据候选人技术背景自动匹配最合适的职位
- **邮件生成**：一键生成完整、可直接发送的中文招聘推介邮件
- **模板自定义**：支持自定义邮件各组成部分（称呼、正文、结尾等）
- **职位管理**：支持新增、编辑、删除、YAML 批量导入导出职位
- **历史消息**：自动保存已生成的邮件记录，支持搜索、编辑、删除、复制
- **双模式使用**：CLI 命令行 + Web 可视化界面
- **Python 库**：可作为 Python 库直接调用，支持自定义 Prompt 和配置

## 快速开始

### 环境要求

- Python >= 3.10
- 可用的 LLM API（通过 [llmdog](https://pypi.org/project/llmdog/) 配置）

### 安装

```bash
pip install cold-msg
```

安装后即可使用 `cold-msg` 命令。

### LLM 配置

ColdMsg 通过 `llmdog` 库调用 LLM，需先配置 LLM API 密钥。请参考 [llmdog 文档](https://pypi.org/project/llmdog/) 进行配置，通常需要设置环境变量或在 `llmdog` 的配置文件中填入 API Key。

> **注意**：如果未正确配置 LLM API，调用 `generate` 命令或 Web 生成邮件时会报错。

### 启动 Web 界面

```bash
cold-msg web
```

默认访问地址：`http://127.0.0.1:6060`

### 快速生成一封邮件

```bash
# 1. 先添加职位
cold-msg jobs add --title "AI技术VP" --qual "大模型，Agent，博士" --loc "北上深"

# 2. 生成邮件
cold-msg generate "张三，10年AI研发经验，精通大模型训练与推理优化"
```

---

## 命令行使用

ColdMsg 提供以下 CLI 子命令：

### `cold-msg generate` - 生成冷邮件

从候选人简历文本生成冷邮件。

```bash
cold-msg generate "张三，10年AI研发经验，精通大模型训练与推理优化"
```

**参数：**

| 参数 | 缩写 | 说明 |
|------|------|------|
| `RESUME` | - | 候选人简历文本（必填） |
| `--model` | `-m` | 指定 LLM 模型名称 |
| `--config` | `-c` | 指定配置文件路径 |
| `--env` | - | 指定 .env 文件路径 |
| `--jobs` | `-j` | 指定职位列表文件路径（yaml/json） |
| `--output` | `-o` | 输出文件路径 |
| `--raw` | - | 输出原始 JSON |

**示例：**

```bash
# 基本用法
cold-msg generate "李四，5年后端开发经验，精通Go和分布式系统"

# 指定模型和输出文件
cold-msg generate "王五，AI算法专家" -m deepseek-chat -o result.json

# 使用外部职位列表
cold-msg generate "赵六，编译器专家" -j positions.yaml

# 输出原始 JSON
cold-msg generate "孙七，前端架构师" --raw
```

### `cold-msg web` - 启动 Web 界面

```bash
cold-msg web
```

**参数：**

| 参数 | 缩写 | 说明 |
|------|------|------|
| `--host` | `-h` | 监听地址（默认 `127.0.0.1`） |
| `--port` | `-p` | 监听端口（默认 `6060`） |
| `--config` | `-c` | 指定配置文件路径 |
| `--env` | - | 指定 .env 文件路径 |
| `--debug/--no-debug` | - | 调试模式 |

**示例：**

```bash
# 指定端口
cold-msg web -p 8080

# 对外开放
cold-msg web --host 0.0.0.0 --port 8080 --no-debug
```

### `cold-msg config` - 查看/管理配置

```bash
# 查看当前配置
cold-msg config show

# 初始化配置文件（生成 config.yaml）
cold-msg config init

# 修改配置项
cold-msg config set llm_model deepseek-chat
cold-msg config set sender_name "张经理"
```

**参数：**

| 参数 | 说明 |
|------|------|
| `ACTION` | 操作：`show` / `init` / `set`（默认 `show`） |
| `KEY` | 配置键名（`set` 操作时使用） |
| `VALUE` | 配置值（`set` 操作时使用） |
| `--config` / `-c` | 指定配置文件路径 |

### `cold-msg jobs` - 管理职位列表

```bash
# 查看职位列表
cold-msg jobs list

# 添加职位
cold-msg jobs add --title "大模型Agent领域技术VP" --qual "Agent，Agentic技术，博士" --loc "北上深"

# 从文件导入
cold-msg jobs import --file positions.yaml

# 导出职位列表
cold-msg jobs export --file jobs_backup.json
```

**参数：**

| 参数 | 缩写 | 说明 |
|------|------|------|
| `ACTION` | - | 操作：`list` / `add` / `import` / `export`（默认 `list`） |
| `--title` | `-t` | 职位名称（`add` 时使用） |
| `--qual` | `-q` | 任职要求（`add` 时使用） |
| `--loc` | `-l` | 工作地点（`add` 时使用） |
| `--file` | `-f` | 文件路径（`import`/`export` 时使用） |
| `--config` | `-c` | 指定配置文件路径 |

### `cold-msg template` - 管理邮件模板

```bash
# 查看当前模板
cold-msg template show

# 修改模板字段
cold-msg template set sender_name "李经理"
cold-msg template set greeting "尊敬的{FullName}，"

# 预览模板效果
cold-msg template preview

# 恢复默认模板
cold-msg template reset
```

**模板字段列表：**

| 字段 | 说明 | 支持的占位符 |
|------|------|-------------|
| `sender_name` | 发件人姓名 | - |
| `team_name` | 招聘团队名称 | - |
| `company_desc` | 公司描述 | - |
| `greeting` | 称呼/开头 | `{FullName}` |
| `intro` | 自我介绍 | `{TeamName}` |
| `body` | 职位推荐正文 | `{CompanyDesc}`, `{JobTitle}` |
| `location_line` | 职位地点行 | `{Location}` |
| `closing` | 结尾联系语 | - |
| `sign_off` | 祝颂语 | - |
| `signature` | 签名 | - |

---

## Python 函数使用

ColdMsg 可作为 Python 库直接调用。

### 基本用法

```python
from cold_msg import generate_cold_email

# 最简调用（自动加载配置）
result = generate_cold_email("张三，10年AI研发经验，精通大模型训练")

if result.match_job_title:
    print(f"推荐职位: {result.recommend_title}")
    print(f"邮件内容:\n{result.email_content}")
else:
    print("未匹配到合适职位")
```

### 自定义配置

```python
from cold_msg import generate_cold_email
from cold_msg.core.config import ColdMsgConfig, JobPosition, EmailTemplate

# 自定义职位列表
jobs = [
    JobPosition(job_title="AI技术VP", qualification="大模型，Agent，博士", location="北上深"),
    JobPosition(job_title="编译器负责人", qualification="LLVM，MLIR，编译优化", location="北上深杭"),
]

# 自定义邮件模板
template = EmailTemplate(
    sender_name="王经理",
    team_name="核心技术招聘部",
    company_desc="全球领先科技公司",
    greeting="尊敬的{FullName}，",
    intro="我来自{TeamName}，通过行业渠道了解到您的专业背景。",
    body="我负责{CompanyDesc}的技术招聘，诚挚向您推荐\"{JobTitle}\"职位。",
    location_line="工作地点：{Location}",
    closing="期待与您进一步交流，欢迎留下联系方式。",
    sign_off="此致敬礼",
    signature="王经理",
)

# 使用自定义配置
cfg = ColdMsgConfig(
    sender_name="王经理",
    team_name="核心技术招聘部",
    company_desc="全球领先科技公司",
    llm_model="deepseek-chat",
    jobs=jobs,
    email_template=template,
)

result = generate_cold_email(
    resume_text="李四，8年编译器开发经验，精通LLVM和MLIR",
    cfg=cfg,
)

print(result.to_json())
```

### 使用自定义 Prompt

```python
from cold_msg import generate_cold_email

# 通过 custom_prompt 覆盖默认的系统提示词
custom_prompt = """你是一位资深猎头，请根据候选人信息匹配最合适的职位，
并以专业但亲切的语气撰写一封中文招聘邮件。返回JSON格式：
{"match_job_title": true/false, "recommend_title": "", "email_content": ""}"""

result = generate_cold_email(
    resume_text="张三，10年AI研发经验",
    custom_prompt=custom_prompt,
)
```

### 从文件生成并保存

```python
from pathlib import Path
from cold_msg.core.generator import generate_cold_email_from_file

result = generate_cold_email_from_file(
    resume_file=Path("resumes/zhangsan.txt"),
    output_dir=Path("output"),  # 匹配成功时结果保存到此目录
)
```

### 核心函数签名

#### `generate_cold_email()`

```python
def generate_cold_email(
    resume_text: str,
    cfg: Optional[ColdMsgConfig] = None,
    jobs: Optional[list[JobPosition]] = None,
    email_template: Optional[EmailTemplate] = None,
    custom_prompt: Optional[str] = None,
    model: Optional[str] = None,
) -> ColdEmailResult
```

**参数说明：**

| 参数 | 类型 | 说明 |
|------|------|------|
| `resume_text` | `str` | 候选人简历文本（必填） |
| `cfg` | `ColdMsgConfig` | 配置实例，为 None 时自动加载 |
| `jobs` | `list[JobPosition]` | 职位列表，为 None 时使用配置中的列表 |
| `email_template` | `EmailTemplate` | 邮件模板，为 None 时使用配置中的模板 |
| `custom_prompt` | `str` | 自定义提示词，覆盖默认系统提示词 |
| `model` | `str` | 指定 LLM 模型名称 |

**返回值：** `ColdEmailResult`

| 字段 | 类型 | 说明 |
|------|------|------|
| `match_job_title` | `bool` | 是否匹配到职位 |
| `recommend_title` | `str` | 推荐职位名称 |
| `email_content` | `str` | 生成的邮件内容 |
| `raw_response` | `dict | None` | LLM 原始响应 |

**方法：**

| 方法 | 说明 |
|------|------|
| `to_dict()` | 转为字典 |
| `to_json(indent=2)` | 转为 JSON 字符串 |
| `from_llm_response(data)` | 从 LLM 返回的字典构建实例（类方法） |

#### `generate_cold_email_from_file()`

```python
def generate_cold_email_from_file(
    resume_file: Path,
    output_dir: Optional[Path] = None,
    cfg: Optional[ColdMsgConfig] = None,
    jobs: Optional[list[JobPosition]] = None,
    model: Optional[str] = None,
) -> ColdEmailResult
```

从简历文件读取文本并生成邮件。当 `output_dir` 不为 None 且匹配成功时，结果以 `{stem}.json` 保存到指定目录。

#### `build_prompt()`

```python
def build_prompt(
    resume_text: str,
    jobs: list[JobPosition],
    email_template: Optional[EmailTemplate] = None,
    custom_prompt: Optional[str] = None,
) -> str
```

构建完整的 LLM Prompt。Prompt 由以下部分组成：

1. **候选人信息** - 简历文本
2. **系统提示词** - 默认或自定义（`custom_prompt`）
3. **邮件示例** - 基于 `EmailTemplate` 生成，保留 `{FullName}`、`{JobTitle}`、`{Location}` 作为 LLM 填充的占位符
4. **内容要求** - 默认的邮件撰写规范（结构、语言风格、禁用词等）
5. **职位列表** - 格式化的职位信息

#### `call_llm()`

```python
def call_llm(
    prompt: str,
    cfg: ColdMsgConfig,
    model: Optional[str] = None,
) -> dict
```

调用 LLM 并返回解析后的字典。内部处理流程：
1. 通过 `llmdog.chat()` 发送 Prompt
2. 清理响应（移除 markdown 代码块标记）
3. 安全解析 JSON
4. 调用失败时返回空字典 `{}`

#### 配置相关函数

```python
from cold_msg.core.config import load_config, save_config, load_jobs_from_file

# 加载配置（自动搜索 .env 和 config.yaml）
cfg = load_config()

# 指定文件加载
cfg = load_config(env_file=Path(".env"), config_file=Path("config.yaml"))

# 保存配置
path = save_config(cfg)  # 默认保存到 ./config.yaml
path = save_config(cfg, config_file=Path("my_config.yaml"))

# 从外部文件加载职位列表（支持 yaml/json）
jobs = load_jobs_from_file(Path("positions.yaml"))
```

### 历史消息 API

```python
from cold_msg.core.history import (
    HistoryRecord, load_history, save_history,
    add_record, update_record, delete_record,
    get_record, search_records,
)

# 加载历史记录
records = load_history()  # 默认从 ./history.json 加载
records = load_history(Path("/path/to/history.json"))  # 指定路径

# 新增记录
record = add_record(
    records,
    candidate_name="张三",
    job_title="AI技术VP",
    email_content="您好张三，...",
    match_job_title=True,
    resume_text="张三，10年AI经验...",
)

# 搜索记录（分页）
result = search_records(records, keyword="张三", page=1, page_size=50)
# 返回: {"records": [...], "total": int, "page": int, "page_size": int, "total_pages": int}
# 搜索范围：title、candidate_name、job_title、email_content
# 默认按 created_at 降序排列，page_size 最大 50

# 获取单条记录
record = get_record(records, record_id="a1b2c3d4e5f6")

# 更新记录（仅更新传入的非 None 字段）
updated = update_record(records, record_id="a1b2c3d4e5f6", candidate_name="张三丰")

# 删除记录
success = delete_record(records, record_id="a1b2c3d4e5f6")

# 手动保存
save_history(records)
```

---

## 数据模型

### `ColdMsgConfig` - 完整配置

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `sender_name` | `str` | `"Michael"` | 发件人姓名 |
| `team_name` | `str` | `"高端招聘团队"` | 招聘团队名称 |
| `company_desc` | `str` | `"国际知名大型科技公司"` | 公司描述 |
| `llm_model` | `str` | `"deepseek-v3.1-terminus"` | LLM 模型名称 |
| `flask_host` | `str` | `"127.0.0.1"` | Web 服务监听地址 |
| `flask_port` | `int` | `6060` | Web 服务监听端口 |
| `flask_debug` | `bool` | `True` | 调试模式 |
| `jobs` | `list[JobPosition]` | `[]` | 职位列表 |
| `email_template` | `EmailTemplate` | `EmailTemplate()` | 邮件模板 |
| `config_dir` | `Path` | `Path.cwd()` | 配置目录 |

### `JobPosition` - 职位信息

| 字段 | 类型 | 说明 |
|------|------|------|
| `job_title` | `str` | 职位名称 |
| `qualification` | `str` | 任职要求 |
| `location` | `str` | 工作地点 |

### `EmailTemplate` - 邮件模板

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `sender_name` | `str` | `"Michael"` | 发件人姓名 |
| `team_name` | `str` | `"高端招聘团队"` | 招聘团队名称 |
| `company_desc` | `str` | `"国际知名大型科技公司"` | 公司描述 |
| `greeting` | `str` | `"您好{FullName}，"` | 称呼/开头 |
| `intro` | `str` | (见代码) | 自我介绍 |
| `body` | `str` | (见代码) | 职位推荐正文 |
| `location_line` | `str` | `"职位地点： {Location}"` | 职位地点行 |
| `closing` | `str` | (见代码) | 结尾联系语 |
| `sign_off` | `str` | `"祝颂商祺"` | 祝颂语 |
| `signature` | `str` | `"Michael"` | 签名 |

### `ColdEmailResult` - 生成结果

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `match_job_title` | `bool` | `False` | 是否匹配到职位 |
| `recommend_title` | `str` | `""` | 推荐职位名称 |
| `email_content` | `str` | `""` | 生成的邮件内容 |
| `raw_response` | `dict | None` | `None` | LLM 原始响应 |

### `HistoryRecord` - 历史消息记录

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `id` | `str` | `""` | 唯一标识（12位hex） |
| `title` | `str` | `""` | 标题（候选人姓名 + 职位） |
| `candidate_name` | `str` | `""` | 候选人姓名 |
| `job_title` | `str` | `""` | 推荐职位 |
| `email_content` | `str` | `""` | 邮件内容 |
| `match_job_title` | `bool` | `False` | 是否匹配到职位 |
| `resume_text` | `str` | `""` | 原始简历文本 |
| `created_at` | `str` | `""` | 创建时间（ISO 格式） |
| `updated_at` | `str` | `""` | 更新时间（ISO 格式） |

---

## 配置说明

### 配置优先级

从高到低：

1. **环境变量**
2. **.env 文件**
3. **配置文件**（`./config.yaml`）
4. **内置默认值**

### 环境变量

| 环境变量 | 对应配置项 | 说明 |
|----------|-----------|------|
| `COLDMSG_SENDER_NAME` | `sender_name` | 发件人姓名 |
| `COLDMSG_TEAM_NAME` | `team_name` | 招聘团队名称 |
| `LLM_MODEL` | `llm_model` | LLM 模型名称 |
| `FLASK_HOST` | `flask_host` | Web 服务监听地址 |
| `FLASK_PORT` | `flask_port` | Web 服务监听端口 |
| `FLASK_DEBUG` | `flask_debug` | 调试模式（`1`/`true`/`yes`） |

> **注意**：`company_desc`、`jobs`、`email_template` 目前仅支持通过配置文件设置，不支持环境变量。

### 配置文件格式（config.yaml）

```yaml
sender_name: "Michael"
team_name: "高端招聘团队"
company_desc: "国际知名大型科技公司"
llm_model: "deepseek-v3.1-terminus"
flask_host: "127.0.0.1"
flask_port: 6060
flask_debug: true

jobs:
  - job_title: "大模型Agent领域技术VP"
    qualification: "Agent，Agentic技术，博士，5年+经验"
    location: "北上深，香港等"
  - job_title: "编译器实验室技术负责人"
    qualification: "编译器技术精通，MLIR，LLVM，GCC，AI编译等"
    location: "北上深杭，香港等"

email_template:
  sender_name: "Michael"
  team_name: "高端招聘团队"
  company_desc: "国际知名大型科技公司"
  greeting: "您好{FullName}，"
  intro: "我来自{TeamName}，很冒昧的给您发邮件。通过Linkedin了解到您，了解到您在XXX, XXX等方向有丰富经验。"
  body: "我负责{CompanyDesc}招聘，非常希望邀请到您这样的人才在此向您推荐\"{JobTitle}\"的职位。负责带领技术团队攻克更多XXX，XXX技术难题，进一步提升您在行业内的影响力。"
  location_line: "职位地点： {Location}"
  closing: "如果您有兴趣具体了解的话，可以留个电话或者微信，给您详细介绍一下，期待与您的沟通~"
  sign_off: "祝颂商祺"
  signature: "Michael"
```

### 职位列表文件格式

支持 YAML 和 JSON 两种格式，也兼容 `JobTitle`/`Qualification`/`Location` 首字母大写的键名：

**YAML 格式（positions.yaml）：**

```yaml
jobs:
  - job_title: "AI算法研究员"
    qualification: "深度学习，NLP，博士优先"
    location: "北京，上海"
  - job_title: "云原生架构师"
    qualification: "Kubernetes，微服务，5年+经验"
    location: "深圳，杭州"
```

**JSON 格式（positions.json）：**

```json
{
  "jobs": [
    {
      "job_title": "AI算法研究员",
      "qualification": "深度学习，NLP，博士优先",
      "location": "北京，上海"
    }
  ]
}
```

> **提示**：职位列表文件也支持 `positions` 作为顶层键名，以及 `JobTitle`/`Qualification`/`Location` 首字母大写的字段名。

### 历史消息存储

历史消息保存在运行目录下的 `history.json` 文件中，项目启动时自动加载，不存在则为空。格式如下：

```json
[
  {
    "id": "a1b2c3d4e5f6",
    "title": "张三 - AI技术VP",
    "candidate_name": "张三",
    "job_title": "AI技术VP",
    "email_content": "您好张三，...",
    "match_job_title": true,
    "resume_text": "张三，10年AI研发经验...",
    "created_at": "2026-07-17T20:00:00.000000",
    "updated_at": "2026-07-17T20:00:00.000000"
  }
]
```

> **注意**：通过 Web 界面生成邮件时，匹配成功的邮件会自动保存到历史记录，但候选人姓名默认为空，需在历史消息页面手动编辑补充。

---

## Web 界面

Web 界面提供 4 个页面：

| 页面 | 路由 | 功能 |
|------|------|------|
| 生成邮件 | `/` | 输入候选人简历文本，一键生成冷邮件，支持复制邮件内容和 JSON |
| 职位管理 | `/jobs` | 新增、编辑、删除职位，YAML 导入导出 |
| 历史消息 | `/history` | 查看已生成邮件记录，搜索、编辑、删除、复制邮件内容 |
| 邮件模板配置 | `/config` | 自定义邮件模板各字段，实时预览效果 |

### Web API

#### 邮件生成

| 路由 | 方法 | 请求体 | 响应 |
|------|------|--------|------|
| `/api/generate` | POST | `{"resume_text": "...", "jobs?": [...], "email_template?": {...}, "model?": "..."}` | `{"match_job_title": bool, "recommend_title": "", "email_content": "", "raw_response": {}}` |

#### 配置管理

| 路由 | 方法 | 请求体 | 响应 |
|------|------|--------|------|
| `/api/config` | GET | - | 完整配置字典 |
| `/api/config` | POST | `{"llm_model?": "...", "jobs?": [...], "email_template?": {...}}` | `{"message": "配置已保存: ..."}` |

#### 职位管理

| 路由 | 方法 | 请求体 | 响应 |
|------|------|--------|------|
| `/api/jobs` | GET | - | `{"jobs": [...]}` |
| `/api/jobs` | POST | `{"jobs": [...]}` | `{"message": "已更新 N 个职位"}` |
| `/api/jobs/import-yaml` | POST | `{"yaml_text": "..."}` | `{"message": "...", "imported": N, "total": N}` |

#### 邮件模板

| 路由 | 方法 | 请求体 | 响应 |
|------|------|--------|------|
| `/api/email-template` | GET | - | `{"email_template": {...}}` |
| `/api/email-template` | POST | `{"email_template": {...}}` | `{"message": "...", "email_template": {...}}` |
| `/api/email-template/preview` | POST | `{"email_template?": {...}}` | `{"preview": "..."}` |

#### 历史消息

| 路由 | 方法 | 请求体 | 响应 |
|------|------|--------|------|
| `/api/history` | GET | - (查询参数: `keyword`, `page`, `page_size`) | `{"records": [...], "total": N, "page": N, "page_size": N, "total_pages": N}` |
| `/api/history` | POST | `{"candidate_name": "...", "job_title": "...", "email_content": "...", ...}` | `HistoryRecord` 字典 (201) |
| `/api/history/<id>` | GET | - | `HistoryRecord` 字典 |
| `/api/history/<id>` | PUT | `{"candidate_name?": "...", "job_title?": "...", ...}` | `HistoryRecord` 字典 |
| `/api/history/<id>` | DELETE | - | `{"message": "已删除"}` |

---

## 项目架构

```
cold_msg/
├── __init__.py              # 包入口，导出 generate_cold_email, __version__
├── cli.py                   # CLI 命令行接口（Typer）
├── core/                    # 核心业务逻辑
│   ├── __init__.py          # 导出 ColdMsgConfig, load_config, generate_cold_email, build_prompt, call_llm
│   ├── config.py            # 配置管理（数据模型 + 加载/保存）
│   ├── generator.py         # 邮件生成器（核心业务入口）
│   ├── history.py           # 历史消息管理（增删改查 + 搜索）
│   ├── llm.py               # LLM 调用封装（基于 llmdog）
│   └── prompt.py            # Prompt 提示词构建
├── web/                     # Web 界面模块
│   ├── __init__.py
│   ├── app.py               # Flask 应用工厂（路由 + API）
│   └── templates/           # Jinja2 HTML 模板
│       ├── index.html       # 主页 - 邮件生成
│       ├── config.html      # 邮件模板配置页
│       ├── history.html     # 历史消息页
│       └── jobs.html        # 职位管理页
├── pyproject.toml           # 项目配置/构建文件
└── docs/
    └── CHANGELOG.md         # 变更记录
```

### 核心模块说明

| 模块 | 职责 |
|------|------|
| `core/config.py` | 定义 `ColdMsgConfig`、`JobPosition`、`EmailTemplate` 数据类；提供 `load_config()`/`save_config()`/`load_jobs_from_file()` 函数；支持环境变量、.env、YAML 多级配置 |
| `core/generator.py` | 核心入口函数 `generate_cold_email()` 和 `generate_cold_email_from_file()`，组合配置、Prompt、LLM 调用生成邮件；定义 `ColdEmailResult` 结果类 |
| `core/prompt.py` | 将候选人简历、职位列表、邮件模板组合为完整 LLM Prompt；定义默认系统提示词、邮件示例和内容要求；提供 `build_prompt()` 函数 |
| `core/llm.py` | 封装 `llmdog` 库调用；提供 `call_llm()` 函数；内部处理响应清理（移除 markdown 代码块标记）和安全 JSON 解析 |
| `core/history.py` | 历史消息的加载、保存、增删改查和搜索；定义 `HistoryRecord` 数据类；数据持久化到 `history.json`；提供 `search_records()` 分页搜索 |
| `web/app.py` | Flask 应用工厂 `create_app()`；定义所有页面路由和 REST API；启动时自动加载 `history.json`；生成邮件后自动保存历史记录 |
| `cli.py` | 基于 Typer 的 CLI 工具；提供 `generate`/`web`/`config`/`jobs`/`template` 五个子命令 |

### 数据流

```
候选人简历文本
    │
    ▼
build_prompt()  ◄── 职位列表 + 邮件模板 + 系统提示词
    │
    ▼
call_llm()  ◄── llmdog → LLM API
    │
    ▼
ColdEmailResult  ──► 自动保存到 history.json（仅 Web 模式）
    │
    ▼
CLI 输出 / Web 页面展示
```

### 技术栈

| 层面 | 技术 |
|------|------|
| 语言 | Python 3.10+ |
| 后端框架 | Flask 3.0+ |
| CLI 框架 | Typer 0.9+（配合 Rich 美化输出） |
| LLM 调用 | llmdog 0.0.3+ |
| 前端 | Tailwind CSS (CDN) + 原生 JavaScript |
| 配置管理 | PyYAML + python-dotenv |
| 数据模型 | Python dataclass |
| 数据持久化 | YAML（配置）+ JSON（历史记录） |
| 代码检查 | Ruff（line-length=100） |
| 测试 | pytest |

---

## 常见问题

### 生成邮件时报错 "LLM 调用失败"

请检查：
1. 是否已正确配置 `llmdog` 的 API Key
2. 网络是否能访问 LLM API 服务
3. 模型名称是否正确（可通过 `cold-msg config set llm_model <model>` 修改）

### 生成结果显示 "未匹配到合适职位"

可能原因：
1. 职位列表为空 - 请先通过 `cold-msg jobs add` 或 Web 界面添加职位
2. 候选人背景与现有职位不匹配 - 尝试添加更多相关职位

### Web 界面无法访问

1. 确认服务已启动：`cold-msg web`
2. 检查端口是否被占用：`cold-msg web -p 8080` 尝试其他端口
3. 如需远程访问，使用 `--host 0.0.0.0`

### 历史消息中候选人姓名为空

通过 Web 界面生成邮件时，候选人姓名默认为空（因为 LLM 返回结果中不包含结构化的姓名字段）。请在历史消息页面点击编辑按钮手动补充候选人姓名。

---

## 开发

### 运行测试

```bash
pip install -e ".[dev]"
pytest
```

### 代码检查

```bash
ruff check .
ruff format .
```

### 项目配置

代码风格和构建配置见 `pyproject.toml`：
- Ruff: `line-length = 100`, `target-version = "py310"`
- Python: `>= 3.10`
- 入口点: `cold-msg = "cold_msg.cli:app"`

---

## 免责声明

1. **本工具仅供合法的招聘沟通用途**。使用者应确保所发送的邮件内容符合当地法律法规，不得用于垃圾邮件、骚扰或任何违法用途。

2. **AI 生成内容可能存在不准确之处**。本工具通过大语言模型自动生成邮件内容，生成结果可能包含事实性错误、措辞不当或其他不适宜内容。使用者应在发送前仔细审核邮件内容，对最终发出的邮件承担全部责任。

3. **候选人隐私保护**。使用本工具处理候选人简历信息时，应遵守相关隐私保护法律法规（如《个人信息保护法》等），确保候选人信息的收集、使用和存储符合法律要求。

4. **LLM API 使用**。本工具依赖第三方大语言模型 API 服务，API 的可用性、稳定性和数据安全由相应服务提供商负责。使用者应自行评估并遵守 API 服务商的使用条款和隐私政策。

5. **数据安全**。本工具将历史记录保存在本地 `history.json` 文件中，配置保存在 `config.yaml` 中。这些文件可能包含候选人简历等敏感信息，使用者应自行负责安全保管，避免敏感信息泄露。建议不要将这些文件提交到公开的版本控制仓库。

6. **无担保声明**。本工具按"原样"提供，不作任何明示或暗示的担保，包括但不限于适销性、特定用途的适用性和非侵权性。在任何情况下，作者或版权持有人均不对因使用本工具而产生的任何索赔、损害或其他责任负责。

---

## License

MIT
