Metadata-Version: 2.4
Name: cold-msg
Version: 0.1.10
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 — 招聘冷邮件智能生成工具

> 喂给它一份简历，它帮你写好一封招聘邮件。

ColdMsg 是一个招聘助手。你把候选人的简历文本丢给它，它会自动判断这个人适合你哪个岗位，然后帮你写好一封可以直接发送的招聘推介邮件（Cold Email）。支持命令行和 Web 界面两种用法。

---

## 它解决什么问题？

想象你是一个猎头，手里有 10 个待招的岗位，每天要翻上百份简历。每看到一位合适的候选人，你都要：

1. 琢磨他适合哪个岗位
2. 打开邮箱，从头写一封推介邮件
3. 措辞、语气、结构都要拿捏好

这件事重复、耗时、还容易写得很套话。ColdMsg 就是把这三步打包交给 AI 来做——**你只管贴简历，它帮你匹配岗位 + 写邮件**。

---

## 它是怎么工作的？

用一个类比来理解：ColdMsg 像一个**很会写招聘信的实习生**。

你交给这个实习生三样东西：
- **候选人的简历**（一张纸）
- **你在招的岗位清单**（一张表）
- **你希望邮件长什么样的模板**（一份范文）

实习生会：
1. 读简历，对照岗位清单，判断这人适合哪个岗
2. 按照你给的范文风格，写一封完整的邮件
3. 把写好的邮件交给你

背后的流程是这样的：

```
你的简历 ──┐
           │
岗位清单 ──┤── 拼成一段"提示词" ──► 交给大模型 ──► 大模型返回 JSON
           │                                          │
邮件模板 ──┘                                          ▼
                                            ┌─────────────────┐
                                            │ 是否匹配到岗位？  │
                                            │ 推荐的岗位名称    │
                                            │ 写好的邮件正文    │
                                            └─────────────────┘
```

大模型（LLM）是"大脑"，负责理解和写作；ColdMsg 是"秘书"，负责把材料整理好递给大脑、再把大脑的回复整理好还给你。

---

## 功能特性

- **智能匹配**：根据候选人背景自动匹配最合适的岗位
- **邮件生成**：一键生成完整、可直接发送的招聘推介邮件
- **中英双语**：支持中文 / 英文邮件，模板与提示词同步切换
- **模板自定义**：称呼、正文、结尾、签名等 10 个字段均可自定义
- **职位管理**：增删改查、启用/禁用、统计摘要、YAML 导入
- **历史消息**：自动保存生成记录，支持搜索、分页、编辑、删除
- **LLM 配置**：Web 界面可配置模型 / API Key / API URL，支持连接测试
- **双模式使用**：CLI 命令行 + Web 可视化界面
- **Python 库**：可作为 Python 库直接调用，支持自定义 Prompt 和配置

---

## 快速开始

### 环境要求

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

### 安装

```bash
pip install -e .
```

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

### 配置 LLM

ColdMsg 通过 `llmdog` 库调用大模型。最简单的方式是设置环境变量：

```bash
# 以 OpenAI 为例
export LLM_API_KEY="sk-xxx"
export LLM_API_URL="https://api.openai.com/v1"
export LLM_MODEL="gpt-4o"
```

也可以在 Web 界面的「LLM 配置」页面填写（详见下文）。

### 启动 Web 界面

```bash
cold-msg web
```

浏览器打开 `http://127.0.0.1:6060`，在首页粘贴简历，点「生成邮件」即可。

### 命令行生成

```bash
# 先添加一个岗位
cold-msg jobs add --title "首席撸猫专家" \
  --qual "撸猫界天花板，下巴挠挠耳后按摩信手拈来" \
  --loc "深圳"

# 贴上简历，生成邮件
cold-msg generate "李四，8年专业撸猫经验，精通英短、布偶、暹罗等15个品种"
```

---

## 命令行使用

ColdMsg 提供 5 个子命令：

| 命令 | 作用 |
|------|------|
| `generate` | 从简历生成冷邮件 |
| `web` | 启动 Web 界面 |
| `config` | 查看 / 管理配置 |
| `jobs` | 管理岗位清单 |
| `template` | 管理邮件模板 |

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

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

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

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

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

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

```bash
cold-msg web                    # 默认 127.0.0.1:6060
cold-msg web -p 8080            # 换端口
cold-msg web -h 0.0.0.0 --no-debug  # 对外开放，关调试
```

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

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

```bash
cold-msg config show                    # 查看当前配置
cold-msg config init                    # 生成 config.yaml
cold-msg config set email_lang en       # 切换为英文邮件
cold-msg config set flask_port 8080     # 改端口
```

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

### `cold-msg jobs` — 管理岗位清单

```bash
cold-msg jobs list                                              # 查看所有岗位
cold-msg jobs add -t "高级遛狗专家" -q "3年以上遛狗经验" -l "上海"  # 添加岗位
cold-msg jobs import -f positions.yaml                          # 从文件导入
cold-msg jobs export -f jobs_backup.json                        # 导出为 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                      # 恢复默认
```

模板共 10 个字段：

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

> **占位符是什么？** 就像填空题。模板里写 `{FullName}`，生成邮件时大模型会把它替换成候选人的真实姓名。`{TeamName}` `{CompanyDesc}` `{SenderName}` 由系统自动替换，`{FullName}` `{JobTitle}` `{Location}` 由大模型根据匹配结果填充。

---

## 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="首席撸猫专家", qualification="5年以上撸猫经验", location="深圳"),
    JobPosition(job_title="高级遛狗专家", qualification="3年以上遛狗经验", 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="全球领先科技公司",
    email_lang="zh",       # "zh" 中文 / "en" 英文
    jobs=jobs,
    email_template=template,
)

result = generate_cold_email(resume_text="李四，8年编译器开发经验", cfg=cfg)
print(result.to_json())
```

### 使用自定义 Prompt

```python
from cold_msg import generate_cold_email

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,
    llm_config: Optional[LlmConfig] = None,
) -> ColdEmailResult
```

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

**返回值** `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 返回的字典构建实例（类方法） |

#### `build_prompt()`

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

把简历、岗位清单、邮件模板拼成一段完整的提示词。提示词由 5 部分组成：

1. **候选人信息** — 简历原文
2. **系统提示词** — 告诉大模型"你是招聘邮件撰写专家"（中/英文两套）
3. **邮件示例** — 基于模板生成，保留 `{FullName}` `{JobTitle}` `{Location}` 让大模型填
4. **内容要求** — 邮件撰写规范（结构、语言风格、禁用词等）
5. **职位列表** — 格式化的岗位信息

#### `call_llm()`

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

调用大模型并返回解析后的字典。内部流程：发送 Prompt → 清理响应（移除 markdown 代码块）→ 解析 JSON → 失败返回 `{}`。

**模型名优先级**：调用方显式指定 > `llm_config.model` > `cfg.llm_model` > llmdog 默认值。

#### 配置相关函数

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

cfg = load_config()                                    # 自动搜索 .env 和 config.yaml
cfg = load_config(env_file=Path(".env"), config_file=Path("config.yaml"))
path = save_config(cfg)                                # 默认保存到 ./config.yaml
jobs = load_jobs_from_file(Path("positions.yaml"))     # 支持 yaml/json
```

### 历史消息 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 加载

record = add_record(                        # 新增记录
    records,
    candidate_name="李四",
    job_title="首席撸猫专家",
    email_content="您好李四，...",
    match_job_title=True,
    resume_text="李四，8年专业撸猫经验...",
)

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")
updated = update_record(records, record_id="a1b2c3d4e5f6", candidate_name="张三丰")
success = delete_record(records, record_id="a1b2c3d4e5f6")
save_history(records)
```

---

## Web 界面

Web 界面提供 5 个页面：

| 页面 | 路由 | 功能 |
|------|------|------|
| 邮件生成 | `/` | 粘贴简历，一键生成冷邮件，支持复制 |
| 职位管理 | `/jobs` | 增删改查、启用/禁用、查看详情、载入示例、统计摘要 |
| 历史消息 | `/history` | 搜索、查看、编辑、删除生成记录 |
| 模板配置 | `/config` | 自定义邮件模板，中英双语切换，实时预览 |
| LLM 配置 | `/llm-config` | 配置模型 / API Key / API URL，连接测试 |

### 职位管理页（`/jobs`）

- **统计栏**：4 列统计 — 总职位 / 有效 / 已禁用 / 待完善
- **启用/禁用开关**：禁用的岗位不参与邮件匹配
- **载入示例**：一键填充创意示例岗位（首席撸猫专家、高级遛狗专家等）
- **查看详情**：点击卡片打开模态框

### 模板配置页（`/config`）

- 10 个模板字段均可在线编辑
- **中英双语切换**：切换时自动加载对应语言的默认模板
  - 中文：小美 / 喵星人人才寻访中心
  - 英文：Amy / Meow Talent Acquisition Center
- 实时预览渲染效果

### LLM 配置页（`/llm-config`）

填写 `model` / `api_key` / `api_url` 三项，点击「测试连接」可验证是否可用。

**配置优先级**：页面配置（三项均非空）> llmdog 默认链（环境变量 > `.llmdog.yaml` > 内置默认值）。三项全空时回退到 llmdog 全局配置。

### Web API

#### 邮件生成

| 路由 | 方法 | 说明 |
|------|------|------|
| `POST /api/generate` | 生成邮件，成功时自动写入历史 | 请求体：`{resume_text, jobs?, email_template?, llm_config?, model?}` |

#### 配置管理

| 路由 | 方法 | 说明 |
|------|------|------|
| `GET /api/config` | 获取完整配置 |
| `POST /api/config` | 保存配置（合并更新） |

#### 职位管理

| 路由 | 方法 | 说明 |
|------|------|------|
| `GET /api/jobs` | 获取职位列表 + 统计摘要 `{jobs, stats: {total, active, disabled, invalid}}` |
| `POST /api/jobs` | 整体更新职位列表（覆盖） |
| `POST /api/jobs/import-yaml` | 从 YAML 文本导入（追加模式） |

#### 邮件模板

| 路由 | 方法 | 说明 |
|------|------|------|
| `GET /api/email-template` | 获取模板 |
| `POST /api/email-template` | 更新模板（合并更新） |
| `POST /api/email-template/preview` | 预览模板（不保存） |

#### LLM 配置

| 路由 | 方法 | 说明 |
|------|------|------|
| `GET /api/llm-config` | 获取 LLM 配置 |
| `POST /api/llm-config` | 保存 LLM 配置 |
| `POST /api/llm-config/test` | 测试连接（发送 ping，返回 `{success, message, model, api_url, latency_ms}`） |

#### 历史消息

| 路由 | 方法 | 说明 |
|------|------|------|
| `GET /api/history` | 列表（查询参数：`keyword`, `page`, `page_size`） |
| `POST /api/history` | 新增记录 |
| `GET /api/history/<id>` | 获取详情 |
| `PUT /api/history/<id>` | 更新记录 |
| `DELETE /api/history/<id>` | 删除记录 |

---

## 配置说明

### 配置优先级

从高到低：

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

> `company_desc`、`email_lang`、`jobs`、`email_template`、`llm_config` 仅从 `config.yaml` 读取，不支持环境变量。

### 环境变量

| 环境变量 | 对应配置项 | 说明 |
|----------|-----------|------|
| `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`） |

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

首次运行时自动生成，完整示例：

```yaml
sender_name: 小美
team_name: 喵星人人才寻访中心
company_desc: 国际领先宠物研究所
email_lang: zh              # "zh" 中文 / "en" 英文
llm_model: ''               # 兜底模型名，为空时用 llmdog 默认
flask_host: 127.0.0.1
flask_port: 6060
flask_debug: true

jobs:
  - job_title: 首席撸猫专家
    qualification: 撸猫界天花板，下巴挠挠耳后按摩信手拈来...
    location: 深圳
    enabled: true           # false 则不参与匹配

email_template:
  sender_name: 小美
  team_name: 喵星人人才寻访中心
  company_desc: 国际领先宠物研究所
  greeting: 您好{FullName}，
  intro: 我来自{TeamName}，很冒昧地给您发这封邮件...
  body: 我负责{CompanyDesc}的招聘工作...
  location_line: 职位地点：{Location}
  closing: 如果您有兴趣进一步了解...
  sign_off: 祝颂商祺
  signature: 小美

llm_config:                 # 三项均非空时覆盖 llmdog 默认配置；全空时省略此块
  model: gpt-4o
  api_key: sk-xxx
  api_url: https://api.openai.com/v1
```

### 职位列表文件格式

支持 YAML 和 JSON，兼容 `JobTitle`/`Qualification`/`Location` 首字母大写键名：

```yaml
# positions.yaml
jobs:
  - job_title: AI算法研究员
    qualification: 深度学习，NLP，博士优先
    location: 北京，上海
    enabled: true
```

```json
// positions.json
{
  "jobs": [
    { "job_title": "AI算法研究员", "qualification": "深度学习，NLP", "location": "北京" }
  ]
}
```

> 也支持 `positions` 作为顶层键名。

### 历史消息存储

历史记录保存在 `history.json`：

```json
[
  {
    "id": "a1b2c3d4e5f6",
    "title": "李四 - 首席撸猫专家",
    "candidate_name": "李四",
    "job_title": "首席撸猫专家",
    "email_content": "您好李四，...",
    "match_job_title": true,
    "resume_text": "李四，8年专业撸猫经验...",
    "created_at": "2026-07-17T20:00:00.000000",
    "updated_at": "2026-07-17T20:00:00.000000"
  }
]
```

> 通过 Web 生成邮件时，匹配成功的结果自动保存，候选人姓名默认为空，可在历史消息页手动补充。

---

## 数据模型

### `ColdMsgConfig` — 完整配置

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `sender_name` | `str` | `"小美"` | 发件人姓名 |
| `team_name` | `str` | `"喵星人人才寻访中心"` | 招聘团队名称 |
| `company_desc` | `str` | `"国际领先宠物研究所"` | 公司描述 |
| `email_lang` | `str` | `"zh"` | 邮件语言：`zh` / `en` |
| `llm_model` | `str` | `""` | LLM 模型名（兜底，为空时用 llmdog 默认） |
| `flask_host` | `str` | `"127.0.0.1"` | Web 监听地址 |
| `flask_port` | `int` | `6060` | Web 监听端口 |
| `flask_debug` | `bool` | `True` | 调试模式 |
| `jobs` | `list[JobPosition]` | (见代码) | 职位列表 |
| `email_template` | `EmailTemplate` | `EmailTemplate()` | 邮件模板 |
| `llm_config` | `LlmConfig` | `LlmConfig()` | LLM 页面配置 |
| `config_dir` | `Path` | `Path.cwd()` | 配置目录 |

### `JobPosition` — 职位信息

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `job_title` | `str` | — | 职位名称 |
| `qualification` | `str` | — | 任职要求 |
| `location` | `str` | — | 工作地点 |
| `enabled` | `bool` | `True` | 是否启用（`False` 时不参与匹配） |

### `EmailTemplate` — 邮件模板

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `sender_name` | `str` | `"小美"` | 发件人姓名 |
| `team_name` | `str` | `"喵星人人才寻访中心"` | 招聘团队名称 |
| `company_desc` | `str` | `"国际领先宠物研究所"` | 公司描述 |
| `greeting` | `str` | `"您好{FullName}，"` | 称呼 / 开头 |
| `intro` | `str` | (见代码) | 自我介绍 |
| `body` | `str` | (见代码) | 职位推荐正文 |
| `location_line` | `str` | `"职位地点：{Location}"` | 职位地点行 |
| `closing` | `str` | (见代码) | 结尾联系语 |
| `sign_off` | `str` | `"祝颂商祺"` | 祝颂语 |
| `signature` | `str` | `"小美"` | 签名 |

### `LlmConfig` — LLM 连接配置

| 字段 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| `model` | `str` | `""` | 模型名称 |
| `api_key` | `str` | `""` | API Key |
| `api_url` | `str` | `""` | API URL |

> 三字段全空 → 回退到 llmdog 默认配置链；三字段均非空 → 以此配置为准。

### `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 格式） |

---

## 项目架构

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

### 核心模块说明

| 模块 | 职责 |
|------|------|
| `core/config.py` | 定义 `ColdMsgConfig`、`JobPosition`、`EmailTemplate`、`LlmConfig` 数据类；提供 `load_config()` / `save_config()` / `load_jobs_from_file()`；支持环境变量、.env、YAML 多级配置 |
| `core/generator.py` | 核心入口 `generate_cold_email()`，组合配置、Prompt、LLM 生成邮件；过滤禁用职位；定义 `ColdEmailResult` |
| `core/prompt.py` | 将简历、职位列表、邮件模板拼成完整 Prompt；中/英文两套系统提示词与内容要求；提供 `build_prompt()` |
| `core/llm.py` | 封装 `llmdog` 调用；提供 `call_llm()` / `test_llm_connection()`；支持 `LlmConfig` 覆盖默认配置；响应清理与 JSON 解析 |
| `core/history.py` | 历史消息的加载、保存、增删改查和分页搜索；定义 `HistoryRecord`；持久化到 `history.json` |
| `web/app.py` | Flask 应用工厂 `create_app()`；定义所有页面路由和 REST API；启动时加载历史；生成后自动保存历史 |
| `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.1.0+ |
| 前端 | Tailwind CSS (CDN) + 原生 JavaScript |
| 配置管理 | PyYAML + python-dotenv |
| 数据模型 | Python dataclass |
| 数据持久化 | YAML（配置）+ JSON（历史记录） |
| 代码检查 | Ruff（line-length=120） |
| 类型检查 | mypy |
| 测试 | pytest |

---

## 常见问题

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

请检查：
1. 是否已正确配置 LLM 的 API Key（环境变量、`.llmdog.yaml` 或 Web 的 LLM 配置页）
2. 网络是否能访问 LLM API 服务
3. 模型名称是否正确（可在 Web LLM 配置页点击「测试连接」验证）

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

可能原因：
1. 职位列表为空 — 先通过 `cold-msg jobs add` 或 Web 界面添加职位
2. 所有职位都被禁用 — 在职位管理页打开启用开关
3. 候选人背景与现有职位不匹配 — 尝试添加更多相关职位

### Web 界面无法访问

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

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

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

### 中英双语怎么切换？

- **CLI**：`cold-msg config set email_lang en`
- **Web**：在「模板配置」页点击语言切换按钮，会自动加载对应语言的默认模板

---

## 开发

### 环境搭建

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

### 运行测试

```bash
pytest
pytest --cov=cold_msg
```

### 代码检查

```bash
ruff check .          # lint
ruff check . --fix    # 自动修复
mypy cold_msg         # 类型检查
```

### 项目配置

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

---

## 免责声明

1. **本工具仅供合法的招聘沟通用途**。不得用于垃圾邮件、骚扰或任何违法用途。

2. **AI 生成内容可能存在不准确之处**。发送前请仔细审核邮件内容，对最终发出的邮件承担全部责任。

3. **候选人隐私保护**。处理候选人简历信息时，应遵守相关隐私保护法律法规（如《个人信息保护法》等）。

4. **LLM API 使用**。本工具依赖第三方大语言模型 API 服务，API 的可用性、稳定性和数据安全由相应服务提供商负责。

5. **数据安全**。`history.json` 和 `config.yaml` 可能包含敏感信息，请妥善保管，建议不要提交到公开的版本控制仓库。

6. **无担保声明**。本工具按"原样"提供，不作任何明示或暗示的担保。

---

## License

MIT
