cold-msg 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {cold_msg-0.1.0 → cold_msg-0.1.2}/PKG-INFO +1 -23
- {cold_msg-0.1.0 → cold_msg-0.1.2}/README.md +0 -21
- cold_msg-0.1.2/cold_msg/core/history.py +296 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/web/app.py +123 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/web/templates/config.html +10 -7
- cold_msg-0.1.2/cold_msg/web/templates/history.html +611 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/web/templates/index.html +1 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/web/templates/jobs.html +1 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg.egg-info/PKG-INFO +1 -23
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg.egg-info/SOURCES.txt +2 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/pyproject.toml +1 -2
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/__init__.py +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/cli.py +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/core/__init__.py +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/core/config.py +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/core/generator.py +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/core/llm.py +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/core/prompt.py +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg/web/__init__.py +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg.egg-info/dependency_links.txt +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg.egg-info/entry_points.txt +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg.egg-info/requires.txt +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/cold_msg.egg-info/top_level.txt +0 -0
- {cold_msg-0.1.0 → cold_msg-0.1.2}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cold-msg
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: 招聘冷邮件智能生成工具 - 基于候选人简历文本自动生成 Cold Email
|
|
5
5
|
Author-email: Chandler <275737875@qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -12,7 +12,6 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Topic :: Communications :: Email
|
|
16
15
|
Requires-Python: >=3.10
|
|
17
16
|
Description-Content-Type: text/markdown
|
|
18
17
|
Requires-Dist: llmdog>=0.1.0
|
|
@@ -45,13 +44,6 @@ Requires-Dist: mypy>=1.0; extra == "dev"
|
|
|
45
44
|
pip install cold-msg
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
或从源码安装:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
git clone https://github.com/example/cold-msg.git
|
|
52
|
-
cd cold-msg
|
|
53
|
-
pip install -e .
|
|
54
|
-
```
|
|
55
47
|
|
|
56
48
|
## 快速开始
|
|
57
49
|
|
|
@@ -223,21 +215,7 @@ cold_msg/
|
|
|
223
215
|
3. **`config.yaml`** — 项目根目录
|
|
224
216
|
4. **内置默认值**
|
|
225
217
|
|
|
226
|
-
## 开发
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
# 安装开发依赖
|
|
230
|
-
pip install -e ".[dev]"
|
|
231
|
-
|
|
232
|
-
# 代码检查
|
|
233
|
-
ruff check .
|
|
234
218
|
|
|
235
|
-
# 类型检查
|
|
236
|
-
mypy cold_msg
|
|
237
|
-
|
|
238
|
-
# 运行测试
|
|
239
|
-
pytest
|
|
240
|
-
```
|
|
241
219
|
|
|
242
220
|
## License
|
|
243
221
|
|
|
@@ -16,13 +16,6 @@
|
|
|
16
16
|
pip install cold-msg
|
|
17
17
|
```
|
|
18
18
|
|
|
19
|
-
或从源码安装:
|
|
20
|
-
|
|
21
|
-
```bash
|
|
22
|
-
git clone https://github.com/example/cold-msg.git
|
|
23
|
-
cd cold-msg
|
|
24
|
-
pip install -e .
|
|
25
|
-
```
|
|
26
19
|
|
|
27
20
|
## 快速开始
|
|
28
21
|
|
|
@@ -194,21 +187,7 @@ cold_msg/
|
|
|
194
187
|
3. **`config.yaml`** — 项目根目录
|
|
195
188
|
4. **内置默认值**
|
|
196
189
|
|
|
197
|
-
## 开发
|
|
198
|
-
|
|
199
|
-
```bash
|
|
200
|
-
# 安装开发依赖
|
|
201
|
-
pip install -e ".[dev]"
|
|
202
|
-
|
|
203
|
-
# 代码检查
|
|
204
|
-
ruff check .
|
|
205
190
|
|
|
206
|
-
# 类型检查
|
|
207
|
-
mypy cold_msg
|
|
208
|
-
|
|
209
|
-
# 运行测试
|
|
210
|
-
pytest
|
|
211
|
-
```
|
|
212
191
|
|
|
213
192
|
## License
|
|
214
193
|
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""历史消息管理模块
|
|
2
|
+
|
|
3
|
+
负责历史消息的加载、保存、增删改查和搜索功能。
|
|
4
|
+
历史数据保存在本地 history.json 文件中。
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import logging
|
|
9
|
+
import uuid
|
|
10
|
+
from dataclasses import dataclass, asdict, field
|
|
11
|
+
from datetime import datetime
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Optional
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class HistoryRecord:
|
|
20
|
+
"""单条历史消息记录"""
|
|
21
|
+
id: str = ""
|
|
22
|
+
title: str = ""
|
|
23
|
+
candidate_name: str = ""
|
|
24
|
+
job_title: str = ""
|
|
25
|
+
email_content: str = ""
|
|
26
|
+
match_job_title: bool = False
|
|
27
|
+
resume_text: str = ""
|
|
28
|
+
created_at: str = ""
|
|
29
|
+
updated_at: str = ""
|
|
30
|
+
|
|
31
|
+
def to_dict(self) -> dict:
|
|
32
|
+
return asdict(self)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _now_iso() -> str:
|
|
36
|
+
"""返回当前时间的 ISO 格式字符串"""
|
|
37
|
+
return datetime.now().isoformat()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _new_id() -> str:
|
|
41
|
+
"""生成唯一 ID"""
|
|
42
|
+
return uuid.uuid4().hex[:12]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_history(history_file: Optional[Path] = None) -> list[HistoryRecord]:
|
|
46
|
+
"""从 history.json 加载历史记录
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
history_file: 历史文件路径,默认为 ./history.json
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
历史记录列表
|
|
53
|
+
"""
|
|
54
|
+
if history_file is None:
|
|
55
|
+
history_file = Path.cwd() / "history.json"
|
|
56
|
+
else:
|
|
57
|
+
history_file = Path(history_file)
|
|
58
|
+
|
|
59
|
+
if not history_file.exists():
|
|
60
|
+
logger.info("历史文件不存在,返回空列表: %s", history_file)
|
|
61
|
+
return []
|
|
62
|
+
|
|
63
|
+
try:
|
|
64
|
+
with open(history_file, "r", encoding="utf-8") as f:
|
|
65
|
+
data = json.load(f)
|
|
66
|
+
except (json.JSONDecodeError, OSError) as e:
|
|
67
|
+
logger.warning("加载历史文件失败: %s,返回空列表", e)
|
|
68
|
+
return []
|
|
69
|
+
|
|
70
|
+
if not isinstance(data, list):
|
|
71
|
+
logger.warning("历史文件格式错误,返回空列表")
|
|
72
|
+
return []
|
|
73
|
+
|
|
74
|
+
records = []
|
|
75
|
+
for item in data:
|
|
76
|
+
if not isinstance(item, dict):
|
|
77
|
+
continue
|
|
78
|
+
records.append(HistoryRecord(
|
|
79
|
+
id=item.get("id", _new_id()),
|
|
80
|
+
title=item.get("title", ""),
|
|
81
|
+
candidate_name=item.get("candidate_name", ""),
|
|
82
|
+
job_title=item.get("job_title", ""),
|
|
83
|
+
email_content=item.get("email_content", ""),
|
|
84
|
+
match_job_title=item.get("match_job_title", False),
|
|
85
|
+
resume_text=item.get("resume_text", ""),
|
|
86
|
+
created_at=item.get("created_at", ""),
|
|
87
|
+
updated_at=item.get("updated_at", ""),
|
|
88
|
+
))
|
|
89
|
+
|
|
90
|
+
return records
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def save_history(records: list[HistoryRecord], history_file: Optional[Path] = None) -> Path:
|
|
94
|
+
"""保存历史记录到 history.json
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
records: 历史记录列表
|
|
98
|
+
history_file: 历史文件路径,默认为 ./history.json
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
保存的文件路径
|
|
102
|
+
"""
|
|
103
|
+
if history_file is None:
|
|
104
|
+
history_file = Path.cwd() / "history.json"
|
|
105
|
+
else:
|
|
106
|
+
history_file = Path(history_file)
|
|
107
|
+
|
|
108
|
+
history_file.parent.mkdir(parents=True, exist_ok=True)
|
|
109
|
+
|
|
110
|
+
data = [r.to_dict() for r in records]
|
|
111
|
+
|
|
112
|
+
with open(history_file, "w", encoding="utf-8") as f:
|
|
113
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
114
|
+
|
|
115
|
+
logger.info("历史记录已保存: %s (%d 条)", history_file, len(records))
|
|
116
|
+
return history_file
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def add_record(
|
|
120
|
+
records: list[HistoryRecord],
|
|
121
|
+
candidate_name: str,
|
|
122
|
+
job_title: str,
|
|
123
|
+
email_content: str,
|
|
124
|
+
match_job_title: bool = False,
|
|
125
|
+
resume_text: str = "",
|
|
126
|
+
history_file: Optional[Path] = None,
|
|
127
|
+
) -> HistoryRecord:
|
|
128
|
+
"""新增一条历史记录并保存
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
records: 当前历史记录列表
|
|
132
|
+
candidate_name: 候选人姓名
|
|
133
|
+
job_title: 推荐职位
|
|
134
|
+
email_content: 邮件内容
|
|
135
|
+
match_job_title: 是否匹配到职位
|
|
136
|
+
resume_text: 原始简历文本
|
|
137
|
+
history_file: 历史文件路径
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
新增的记录
|
|
141
|
+
"""
|
|
142
|
+
now = _now_iso()
|
|
143
|
+
title = f"{candidate_name} - {job_title}" if candidate_name and job_title else candidate_name or job_title or "未命名"
|
|
144
|
+
|
|
145
|
+
record = HistoryRecord(
|
|
146
|
+
id=_new_id(),
|
|
147
|
+
title=title,
|
|
148
|
+
candidate_name=candidate_name,
|
|
149
|
+
job_title=job_title,
|
|
150
|
+
email_content=email_content,
|
|
151
|
+
match_job_title=match_job_title,
|
|
152
|
+
resume_text=resume_text,
|
|
153
|
+
created_at=now,
|
|
154
|
+
updated_at=now,
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
records.insert(0, record)
|
|
158
|
+
save_history(records, history_file)
|
|
159
|
+
return record
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def update_record(
|
|
163
|
+
records: list[HistoryRecord],
|
|
164
|
+
record_id: str,
|
|
165
|
+
candidate_name: Optional[str] = None,
|
|
166
|
+
job_title: Optional[str] = None,
|
|
167
|
+
email_content: Optional[str] = None,
|
|
168
|
+
match_job_title: Optional[bool] = None,
|
|
169
|
+
resume_text: Optional[str] = None,
|
|
170
|
+
history_file: Optional[Path] = None,
|
|
171
|
+
) -> Optional[HistoryRecord]:
|
|
172
|
+
"""更新一条历史记录
|
|
173
|
+
|
|
174
|
+
Args:
|
|
175
|
+
records: 当前历史记录列表
|
|
176
|
+
record_id: 记录 ID
|
|
177
|
+
其他参数为可选更新字段
|
|
178
|
+
|
|
179
|
+
Returns:
|
|
180
|
+
更新后的记录,未找到返回 None
|
|
181
|
+
"""
|
|
182
|
+
for r in records:
|
|
183
|
+
if r.id == record_id:
|
|
184
|
+
if candidate_name is not None:
|
|
185
|
+
r.candidate_name = candidate_name
|
|
186
|
+
if job_title is not None:
|
|
187
|
+
r.job_title = job_title
|
|
188
|
+
if email_content is not None:
|
|
189
|
+
r.email_content = email_content
|
|
190
|
+
if match_job_title is not None:
|
|
191
|
+
r.match_job_title = match_job_title
|
|
192
|
+
if resume_text is not None:
|
|
193
|
+
r.resume_text = resume_text
|
|
194
|
+
r.title = f"{r.candidate_name} - {r.job_title}" if r.candidate_name and r.job_title else r.candidate_name or r.job_title or "未命名"
|
|
195
|
+
r.updated_at = _now_iso()
|
|
196
|
+
save_history(records, history_file)
|
|
197
|
+
return r
|
|
198
|
+
|
|
199
|
+
return None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def delete_record(
|
|
203
|
+
records: list[HistoryRecord],
|
|
204
|
+
record_id: str,
|
|
205
|
+
history_file: Optional[Path] = None,
|
|
206
|
+
) -> bool:
|
|
207
|
+
"""删除一条历史记录
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
records: 当前历史记录列表
|
|
211
|
+
record_id: 记录 ID
|
|
212
|
+
history_file: 历史文件路径
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
是否删除成功
|
|
216
|
+
"""
|
|
217
|
+
original_len = len(records)
|
|
218
|
+
records[:] = [r for r in records if r.id != record_id]
|
|
219
|
+
if len(records) < original_len:
|
|
220
|
+
save_history(records, history_file)
|
|
221
|
+
return True
|
|
222
|
+
return False
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def get_record(records: list[HistoryRecord], record_id: str) -> Optional[HistoryRecord]:
|
|
226
|
+
"""根据 ID 获取单条记录
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
records: 当前历史记录列表
|
|
230
|
+
record_id: 记录 ID
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
记录实例,未找到返回 None
|
|
234
|
+
"""
|
|
235
|
+
for r in records:
|
|
236
|
+
if r.id == record_id:
|
|
237
|
+
return r
|
|
238
|
+
return None
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def search_records(
|
|
242
|
+
records: list[HistoryRecord],
|
|
243
|
+
keyword: str = "",
|
|
244
|
+
page: int = 1,
|
|
245
|
+
page_size: int = 50,
|
|
246
|
+
) -> dict:
|
|
247
|
+
"""搜索历史记录(支持分页)
|
|
248
|
+
|
|
249
|
+
按时间降序排列,支持按 title、候选人姓名、职位、邮件内容检索。
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
records: 当前历史记录列表
|
|
253
|
+
keyword: 搜索关键词
|
|
254
|
+
page: 页码(从 1 开始)
|
|
255
|
+
page_size: 每页条数(最大 50)
|
|
256
|
+
|
|
257
|
+
Returns:
|
|
258
|
+
{"records": [...], "total": int, "page": int, "page_size": int, "total_pages": int}
|
|
259
|
+
"""
|
|
260
|
+
if page_size > 50:
|
|
261
|
+
page_size = 50
|
|
262
|
+
if page_size < 1:
|
|
263
|
+
page_size = 50
|
|
264
|
+
if page < 1:
|
|
265
|
+
page = 1
|
|
266
|
+
|
|
267
|
+
sorted_records = sorted(records, key=lambda r: r.created_at, reverse=True)
|
|
268
|
+
|
|
269
|
+
if keyword and keyword.strip():
|
|
270
|
+
kw = keyword.strip().lower()
|
|
271
|
+
filtered = []
|
|
272
|
+
for r in sorted_records:
|
|
273
|
+
if (kw in r.title.lower()
|
|
274
|
+
or kw in r.candidate_name.lower()
|
|
275
|
+
or kw in r.job_title.lower()
|
|
276
|
+
or kw in r.email_content.lower()):
|
|
277
|
+
filtered.append(r)
|
|
278
|
+
sorted_records = filtered
|
|
279
|
+
|
|
280
|
+
total = len(sorted_records)
|
|
281
|
+
total_pages = max(1, (total + page_size - 1) // page_size)
|
|
282
|
+
|
|
283
|
+
if page > total_pages:
|
|
284
|
+
page = total_pages
|
|
285
|
+
|
|
286
|
+
start = (page - 1) * page_size
|
|
287
|
+
end = start + page_size
|
|
288
|
+
page_records = sorted_records[start:end]
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
"records": [r.to_dict() for r in page_records],
|
|
292
|
+
"total": total,
|
|
293
|
+
"page": page,
|
|
294
|
+
"page_size": page_size,
|
|
295
|
+
"total_pages": total_pages,
|
|
296
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
- 配置职位列表
|
|
6
6
|
- 配置邮件模板
|
|
7
7
|
- 一键生成冷邮件
|
|
8
|
+
- 历史消息管理
|
|
8
9
|
"""
|
|
9
10
|
|
|
10
11
|
import json
|
|
@@ -13,6 +14,10 @@ from flask import Flask, render_template, request, jsonify
|
|
|
13
14
|
|
|
14
15
|
from cold_msg.core.config import ColdMsgConfig, JobPosition, EmailTemplate, load_config, save_config
|
|
15
16
|
from cold_msg.core.generator import generate_cold_email
|
|
17
|
+
from cold_msg.core.history import (
|
|
18
|
+
HistoryRecord, load_history, save_history, add_record,
|
|
19
|
+
update_record, delete_record, get_record, search_records,
|
|
20
|
+
)
|
|
16
21
|
|
|
17
22
|
|
|
18
23
|
def create_app(cfg: ColdMsgConfig | None = None) -> Flask:
|
|
@@ -35,6 +40,10 @@ def create_app(cfg: ColdMsgConfig | None = None) -> Flask:
|
|
|
35
40
|
|
|
36
41
|
app.config["COLDMSG_CFG"] = cfg
|
|
37
42
|
|
|
43
|
+
# 启动时加载历史记录
|
|
44
|
+
history_records: list[HistoryRecord] = load_history()
|
|
45
|
+
app.config["COLDMSG_HISTORY"] = history_records
|
|
46
|
+
|
|
38
47
|
@app.route("/")
|
|
39
48
|
def index():
|
|
40
49
|
"""主页 - 邮件生成"""
|
|
@@ -93,6 +102,19 @@ def create_app(cfg: ColdMsgConfig | None = None) -> Flask:
|
|
|
93
102
|
email_template=email_template,
|
|
94
103
|
model=model,
|
|
95
104
|
)
|
|
105
|
+
|
|
106
|
+
# 自动保存到历史记录
|
|
107
|
+
if result.match_job_title and result.email_content:
|
|
108
|
+
records: list[HistoryRecord] = app.config["COLDMSG_HISTORY"]
|
|
109
|
+
add_record(
|
|
110
|
+
records,
|
|
111
|
+
candidate_name="",
|
|
112
|
+
job_title=result.recommend_title,
|
|
113
|
+
email_content=result.email_content,
|
|
114
|
+
match_job_title=result.match_job_title,
|
|
115
|
+
resume_text=resume_text,
|
|
116
|
+
)
|
|
117
|
+
|
|
96
118
|
return jsonify(result.to_dict())
|
|
97
119
|
except Exception as e:
|
|
98
120
|
return jsonify({"error": f"生成失败: {str(e)}"}), 500
|
|
@@ -276,4 +298,105 @@ def create_app(cfg: ColdMsgConfig | None = None) -> Flask:
|
|
|
276
298
|
|
|
277
299
|
return jsonify({"preview": text})
|
|
278
300
|
|
|
301
|
+
# ==================== 历史消息 ====================
|
|
302
|
+
|
|
303
|
+
@app.route("/history")
|
|
304
|
+
def history_page():
|
|
305
|
+
"""历史消息页面"""
|
|
306
|
+
return render_template("history.html")
|
|
307
|
+
|
|
308
|
+
@app.route("/api/history", methods=["GET"])
|
|
309
|
+
def api_get_history():
|
|
310
|
+
"""获取历史消息列表(分页 + 搜索)"""
|
|
311
|
+
records: list[HistoryRecord] = app.config["COLDMSG_HISTORY"]
|
|
312
|
+
keyword = request.args.get("keyword", "").strip()
|
|
313
|
+
page = request.args.get("page", "1")
|
|
314
|
+
page_size = request.args.get("page_size", "50")
|
|
315
|
+
|
|
316
|
+
try:
|
|
317
|
+
page = int(page)
|
|
318
|
+
except (ValueError, TypeError):
|
|
319
|
+
page = 1
|
|
320
|
+
|
|
321
|
+
try:
|
|
322
|
+
page_size = int(page_size)
|
|
323
|
+
except (ValueError, TypeError):
|
|
324
|
+
page_size = 50
|
|
325
|
+
|
|
326
|
+
result = search_records(records, keyword=keyword, page=page, page_size=page_size)
|
|
327
|
+
return jsonify(result)
|
|
328
|
+
|
|
329
|
+
@app.route("/api/history/<record_id>", methods=["GET"])
|
|
330
|
+
def api_get_history_detail(record_id):
|
|
331
|
+
"""获取单条历史消息详情"""
|
|
332
|
+
records: list[HistoryRecord] = app.config["COLDMSG_HISTORY"]
|
|
333
|
+
record = get_record(records, record_id)
|
|
334
|
+
if record is None:
|
|
335
|
+
return jsonify({"error": "记录不存在"}), 404
|
|
336
|
+
return jsonify(record.to_dict())
|
|
337
|
+
|
|
338
|
+
@app.route("/api/history", methods=["POST"])
|
|
339
|
+
def api_add_history():
|
|
340
|
+
"""新增历史消息"""
|
|
341
|
+
data = request.get_json()
|
|
342
|
+
if not data:
|
|
343
|
+
return jsonify({"error": "请求数据为空"}), 400
|
|
344
|
+
|
|
345
|
+
records: list[HistoryRecord] = app.config["COLDMSG_HISTORY"]
|
|
346
|
+
|
|
347
|
+
candidate_name = data.get("candidate_name", "").strip()
|
|
348
|
+
job_title = data.get("job_title", "").strip()
|
|
349
|
+
email_content = data.get("email_content", "").strip()
|
|
350
|
+
match_job_title = data.get("match_job_title", False)
|
|
351
|
+
resume_text = data.get("resume_text", "")
|
|
352
|
+
|
|
353
|
+
if not email_content:
|
|
354
|
+
return jsonify({"error": "邮件内容不能为空"}), 400
|
|
355
|
+
|
|
356
|
+
record = add_record(
|
|
357
|
+
records,
|
|
358
|
+
candidate_name=candidate_name,
|
|
359
|
+
job_title=job_title,
|
|
360
|
+
email_content=email_content,
|
|
361
|
+
match_job_title=match_job_title,
|
|
362
|
+
resume_text=resume_text,
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
return jsonify(record.to_dict()), 201
|
|
366
|
+
|
|
367
|
+
@app.route("/api/history/<record_id>", methods=["PUT"])
|
|
368
|
+
def api_update_history(record_id):
|
|
369
|
+
"""更新历史消息"""
|
|
370
|
+
data = request.get_json()
|
|
371
|
+
if not data:
|
|
372
|
+
return jsonify({"error": "请求数据为空"}), 400
|
|
373
|
+
|
|
374
|
+
records: list[HistoryRecord] = app.config["COLDMSG_HISTORY"]
|
|
375
|
+
|
|
376
|
+
record = update_record(
|
|
377
|
+
records,
|
|
378
|
+
record_id,
|
|
379
|
+
candidate_name=data.get("candidate_name"),
|
|
380
|
+
job_title=data.get("job_title"),
|
|
381
|
+
email_content=data.get("email_content"),
|
|
382
|
+
match_job_title=data.get("match_job_title"),
|
|
383
|
+
resume_text=data.get("resume_text"),
|
|
384
|
+
)
|
|
385
|
+
|
|
386
|
+
if record is None:
|
|
387
|
+
return jsonify({"error": "记录不存在"}), 404
|
|
388
|
+
|
|
389
|
+
return jsonify(record.to_dict())
|
|
390
|
+
|
|
391
|
+
@app.route("/api/history/<record_id>", methods=["DELETE"])
|
|
392
|
+
def api_delete_history(record_id):
|
|
393
|
+
"""删除历史消息"""
|
|
394
|
+
records: list[HistoryRecord] = app.config["COLDMSG_HISTORY"]
|
|
395
|
+
|
|
396
|
+
success = delete_record(records, record_id)
|
|
397
|
+
if not success:
|
|
398
|
+
return jsonify({"error": "记录不存在"}), 404
|
|
399
|
+
|
|
400
|
+
return jsonify({"message": "已删除"})
|
|
401
|
+
|
|
279
402
|
return app
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
<div class="flex space-x-4">
|
|
25
25
|
<a href="/" class="text-gray-500 hover:text-gray-700 text-sm">生成邮件</a>
|
|
26
26
|
<a href="/jobs" class="text-gray-500 hover:text-gray-700 text-sm">职位管理</a>
|
|
27
|
+
<a href="/history" class="text-gray-500 hover:text-gray-700 text-sm">历史消息</a>
|
|
27
28
|
<a href="/config" class="text-blue-600 font-medium text-sm">邮件模板配置</a>
|
|
28
29
|
</div>
|
|
29
30
|
</div>
|
|
@@ -40,6 +41,15 @@
|
|
|
40
41
|
</button>
|
|
41
42
|
</div>
|
|
42
43
|
|
|
44
|
+
|
|
45
|
+
<!-- 保存按钮 -->
|
|
46
|
+
<div class="flex justify-end space-x-3">
|
|
47
|
+
<button id="saveBtn"
|
|
48
|
+
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-6 rounded-lg transition-colors text-sm">
|
|
49
|
+
保存配置
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
|
|
43
53
|
<!-- 发件人信息 -->
|
|
44
54
|
<div class="mb-5">
|
|
45
55
|
<h3 class="text-sm font-medium text-gray-600 mb-3">发件人信息</h3>
|
|
@@ -147,13 +157,6 @@
|
|
|
147
157
|
</div>
|
|
148
158
|
</div>
|
|
149
159
|
|
|
150
|
-
<!-- 保存按钮 -->
|
|
151
|
-
<div class="flex justify-end space-x-3">
|
|
152
|
-
<button id="saveBtn"
|
|
153
|
-
class="bg-blue-600 hover:bg-blue-700 text-white font-medium py-2.5 px-6 rounded-lg transition-colors text-sm">
|
|
154
|
-
保存配置
|
|
155
|
-
</button>
|
|
156
|
-
</div>
|
|
157
160
|
|
|
158
161
|
<!-- 配置说明 -->
|
|
159
162
|
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-5">
|
|
@@ -0,0 +1,611 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="zh-CN">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>ColdMsg - 历史消息</title>
|
|
7
|
+
<script src="https://cdn.tailwindcss.com"></script>
|
|
8
|
+
<style>
|
|
9
|
+
.fade-in { animation: fadeIn 0.2s ease-in; }
|
|
10
|
+
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
|
|
11
|
+
.modal-overlay { background: rgba(0,0,0,0.4); }
|
|
12
|
+
.line-clamp-2 {
|
|
13
|
+
display: -webkit-box;
|
|
14
|
+
-webkit-line-clamp: 2;
|
|
15
|
+
-webkit-box-orient: vertical;
|
|
16
|
+
overflow: hidden;
|
|
17
|
+
}
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body class="bg-gray-50 min-h-screen">
|
|
21
|
+
<!-- 导航栏 -->
|
|
22
|
+
<nav class="bg-white border-b border-gray-200 shadow-sm">
|
|
23
|
+
<div class="max-w-6xl mx-auto px-4 py-3 flex items-center justify-between">
|
|
24
|
+
<div class="flex items-center space-x-3">
|
|
25
|
+
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center">
|
|
26
|
+
<span class="text-white font-bold text-sm">CM</span>
|
|
27
|
+
</div>
|
|
28
|
+
<span class="text-xl font-semibold text-gray-800">ColdMsg</span>
|
|
29
|
+
<span class="text-xs text-gray-400 ml-1">招聘冷邮件生成</span>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="flex space-x-4">
|
|
32
|
+
<a href="/" class="text-gray-500 hover:text-gray-700 text-sm">生成邮件</a>
|
|
33
|
+
<a href="/jobs" class="text-gray-500 hover:text-gray-700 text-sm">职位管理</a>
|
|
34
|
+
<a href="/history" class="text-blue-600 font-medium text-sm">历史消息</a>
|
|
35
|
+
<a href="/config" class="text-gray-500 hover:text-gray-700 text-sm">邮件模板配置</a>
|
|
36
|
+
</div>
|
|
37
|
+
</div>
|
|
38
|
+
</nav>
|
|
39
|
+
|
|
40
|
+
<div class="max-w-6xl mx-auto px-4 py-6">
|
|
41
|
+
<!-- 顶部操作栏 -->
|
|
42
|
+
<div class="flex items-center justify-between mb-5">
|
|
43
|
+
<div>
|
|
44
|
+
<h1 class="text-lg font-semibold text-gray-800">历史消息</h1>
|
|
45
|
+
<p class="text-xs text-gray-400 mt-1">查看已生成的冷邮件记录,支持搜索、编辑和删除</p>
|
|
46
|
+
</div>
|
|
47
|
+
<div class="flex items-center space-x-3">
|
|
48
|
+
<span id="totalCount" class="text-sm text-gray-500">共 0 条</span>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
|
|
52
|
+
<!-- 搜索栏 -->
|
|
53
|
+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 mb-5">
|
|
54
|
+
<div class="flex items-center space-x-3">
|
|
55
|
+
<div class="flex-1 relative">
|
|
56
|
+
<svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
57
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/>
|
|
58
|
+
</svg>
|
|
59
|
+
<input id="searchInput" type="text"
|
|
60
|
+
class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
61
|
+
placeholder="搜索候选人姓名、职位、邮件内容...">
|
|
62
|
+
</div>
|
|
63
|
+
<button id="searchBtn"
|
|
64
|
+
class="bg-blue-600 hover:bg-blue-700 text-white text-sm font-medium py-2 px-4 rounded-lg transition-colors">
|
|
65
|
+
搜索
|
|
66
|
+
</button>
|
|
67
|
+
<button id="clearSearchBtn"
|
|
68
|
+
class="bg-white hover:bg-gray-50 text-gray-600 border border-gray-300 text-sm font-medium py-2 px-4 rounded-lg transition-colors">
|
|
69
|
+
清除
|
|
70
|
+
</button>
|
|
71
|
+
</div>
|
|
72
|
+
</div>
|
|
73
|
+
|
|
74
|
+
<!-- 历史记录列表 -->
|
|
75
|
+
<div id="historyContainer" class="space-y-3">
|
|
76
|
+
<!-- 动态渲染 -->
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<!-- 空状态 -->
|
|
80
|
+
<div id="emptyState" class="bg-white rounded-lg shadow-sm border border-gray-200 p-5 hidden">
|
|
81
|
+
<div class="text-center py-12">
|
|
82
|
+
<div class="w-20 h-20 bg-gray-50 rounded-full flex items-center justify-center mx-auto mb-5">
|
|
83
|
+
<svg class="w-10 h-10 text-gray-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
84
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
85
|
+
</svg>
|
|
86
|
+
</div>
|
|
87
|
+
<p class="text-gray-400 text-sm">暂无历史消息</p>
|
|
88
|
+
<p class="text-gray-400 text-sm">生成邮件后将自动保存到此处</p>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<!-- 分页 -->
|
|
93
|
+
<div id="pagination" class="flex items-center justify-between mt-5 hidden">
|
|
94
|
+
<div class="text-sm text-gray-500">
|
|
95
|
+
<span id="pageInfo">第 1 页 / 共 1 页</span>
|
|
96
|
+
</div>
|
|
97
|
+
<div class="flex items-center space-x-2">
|
|
98
|
+
<button id="prevPageBtn"
|
|
99
|
+
class="px-3 py-1.5 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
100
|
+
disabled>
|
|
101
|
+
上一页
|
|
102
|
+
</button>
|
|
103
|
+
<button id="nextPageBtn"
|
|
104
|
+
class="px-3 py-1.5 text-sm border border-gray-300 rounded-lg hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
|
|
105
|
+
disabled>
|
|
106
|
+
下一页
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
</div>
|
|
110
|
+
</div>
|
|
111
|
+
|
|
112
|
+
<!-- 详情模态框 -->
|
|
113
|
+
<div id="detailModal" class="fixed inset-0 z-50 hidden">
|
|
114
|
+
<div class="modal-overlay absolute inset-0" id="detailModalOverlay"></div>
|
|
115
|
+
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
116
|
+
<div class="bg-white rounded-xl shadow-xl w-full max-w-2xl max-h-[90vh] flex flex-col relative fade-in">
|
|
117
|
+
<!-- 模态框头部 -->
|
|
118
|
+
<div class="flex items-center justify-between px-6 py-4 border-b border-gray-100 flex-shrink-0">
|
|
119
|
+
<div class="flex items-center space-x-3">
|
|
120
|
+
<h3 id="detailTitle" class="text-base font-semibold text-gray-800"></h3>
|
|
121
|
+
<div id="detailMatchBadge"></div>
|
|
122
|
+
</div>
|
|
123
|
+
<div class="flex items-center space-x-2">
|
|
124
|
+
<button id="detailCopyBtn"
|
|
125
|
+
class="text-xs text-gray-500 hover:text-blue-600 transition-colors px-2 py-1 rounded hover:bg-gray-50">
|
|
126
|
+
复制邮件
|
|
127
|
+
</button>
|
|
128
|
+
<button id="detailCloseBtn"
|
|
129
|
+
class="text-gray-400 hover:text-gray-600 p-1 rounded-lg hover:bg-gray-50 transition-colors">
|
|
130
|
+
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
131
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
132
|
+
</svg>
|
|
133
|
+
</button>
|
|
134
|
+
</div>
|
|
135
|
+
</div>
|
|
136
|
+
<!-- 模态框内容 -->
|
|
137
|
+
<div class="flex-1 overflow-y-auto px-6 py-4">
|
|
138
|
+
<div class="space-y-4">
|
|
139
|
+
<!-- 基本信息 -->
|
|
140
|
+
<div class="grid grid-cols-2 gap-4">
|
|
141
|
+
<div>
|
|
142
|
+
<label class="block text-xs text-gray-400 mb-1">候选人姓名</label>
|
|
143
|
+
<div id="detailCandidateName" class="text-sm text-gray-700"></div>
|
|
144
|
+
</div>
|
|
145
|
+
<div>
|
|
146
|
+
<label class="block text-xs text-gray-400 mb-1">推荐职位</label>
|
|
147
|
+
<div id="detailJobTitle" class="text-sm text-gray-700"></div>
|
|
148
|
+
</div>
|
|
149
|
+
</div>
|
|
150
|
+
<div class="grid grid-cols-2 gap-4">
|
|
151
|
+
<div>
|
|
152
|
+
<label class="block text-xs text-gray-400 mb-1">创建时间</label>
|
|
153
|
+
<div id="detailCreatedAt" class="text-sm text-gray-700"></div>
|
|
154
|
+
</div>
|
|
155
|
+
<div>
|
|
156
|
+
<label class="block text-xs text-gray-400 mb-1">更新时间</label>
|
|
157
|
+
<div id="detailUpdatedAt" class="text-sm text-gray-700"></div>
|
|
158
|
+
</div>
|
|
159
|
+
</div>
|
|
160
|
+
<!-- 邮件内容 -->
|
|
161
|
+
<div>
|
|
162
|
+
<label class="block text-xs text-gray-400 mb-1">邮件内容</label>
|
|
163
|
+
<div id="detailEmailContent" class="bg-gray-50 rounded-lg p-4 text-sm text-gray-700 leading-relaxed whitespace-pre-wrap border border-gray-100"></div>
|
|
164
|
+
</div>
|
|
165
|
+
<!-- 简历文本 -->
|
|
166
|
+
<div>
|
|
167
|
+
<label class="block text-xs text-gray-400 mb-1">原始简历文本</label>
|
|
168
|
+
<div id="detailResumeText" class="bg-gray-50 rounded-lg p-4 text-sm text-gray-600 leading-relaxed whitespace-pre-wrap border border-gray-100 max-h-48 overflow-y-auto"></div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
</div>
|
|
172
|
+
<!-- 模态框底部 -->
|
|
173
|
+
<div class="flex items-center justify-between px-6 py-4 border-t border-gray-100 flex-shrink-0">
|
|
174
|
+
<div class="flex space-x-2">
|
|
175
|
+
<button id="detailEditBtn"
|
|
176
|
+
class="px-3 py-1.5 text-sm text-blue-600 hover:text-blue-800 border border-blue-300 rounded-lg hover:bg-blue-50 transition-colors">
|
|
177
|
+
编辑
|
|
178
|
+
</button>
|
|
179
|
+
<button id="detailDeleteBtn"
|
|
180
|
+
class="px-3 py-1.5 text-sm text-red-600 hover:text-red-800 border border-red-300 rounded-lg hover:bg-red-50 transition-colors">
|
|
181
|
+
删除
|
|
182
|
+
</button>
|
|
183
|
+
</div>
|
|
184
|
+
<button id="detailCloseBtn2"
|
|
185
|
+
class="px-4 py-1.5 text-sm text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg transition-colors">
|
|
186
|
+
关闭
|
|
187
|
+
</button>
|
|
188
|
+
</div>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<!-- 编辑模态框 -->
|
|
194
|
+
<div id="editModal" class="fixed inset-0 z-50 hidden">
|
|
195
|
+
<div class="modal-overlay absolute inset-0" id="editModalOverlay"></div>
|
|
196
|
+
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
197
|
+
<div class="bg-white rounded-xl shadow-xl w-full max-w-lg p-6 relative fade-in">
|
|
198
|
+
<h3 class="text-base font-semibold text-gray-800 mb-4">编辑历史消息</h3>
|
|
199
|
+
<div class="space-y-4">
|
|
200
|
+
<div>
|
|
201
|
+
<label class="block text-sm font-medium text-gray-600 mb-1">候选人姓名</label>
|
|
202
|
+
<input id="editCandidateName" type="text"
|
|
203
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
204
|
+
placeholder="候选人姓名">
|
|
205
|
+
</div>
|
|
206
|
+
<div>
|
|
207
|
+
<label class="block text-sm font-medium text-gray-600 mb-1">推荐职位</label>
|
|
208
|
+
<input id="editJobTitle" type="text"
|
|
209
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500"
|
|
210
|
+
placeholder="推荐职位">
|
|
211
|
+
</div>
|
|
212
|
+
<div>
|
|
213
|
+
<label class="block text-sm font-medium text-gray-600 mb-1">邮件内容</label>
|
|
214
|
+
<textarea id="editEmailContent" rows="8"
|
|
215
|
+
class="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 focus:border-blue-500 resize-y"
|
|
216
|
+
placeholder="邮件内容"></textarea>
|
|
217
|
+
</div>
|
|
218
|
+
</div>
|
|
219
|
+
<div class="flex justify-end space-x-3 mt-6">
|
|
220
|
+
<button id="editCancelBtn"
|
|
221
|
+
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg transition-colors">
|
|
222
|
+
取消
|
|
223
|
+
</button>
|
|
224
|
+
<button id="editSaveBtn"
|
|
225
|
+
class="px-4 py-2 text-sm text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors">
|
|
226
|
+
保存
|
|
227
|
+
</button>
|
|
228
|
+
</div>
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
</div>
|
|
232
|
+
|
|
233
|
+
<!-- 删除确认弹窗 -->
|
|
234
|
+
<div id="deleteModal" class="fixed inset-0 z-50 hidden">
|
|
235
|
+
<div class="modal-overlay absolute inset-0" id="deleteModalOverlay"></div>
|
|
236
|
+
<div class="absolute inset-0 flex items-center justify-center p-4">
|
|
237
|
+
<div class="bg-white rounded-xl shadow-xl w-full max-w-sm p-6 relative fade-in">
|
|
238
|
+
<h3 class="text-base font-semibold text-gray-800 mb-2">确认删除</h3>
|
|
239
|
+
<p class="text-sm text-gray-500 mb-5">确定要删除该历史消息吗?此操作不可撤销。</p>
|
|
240
|
+
<div class="flex justify-end space-x-3">
|
|
241
|
+
<button id="deleteCancelBtn"
|
|
242
|
+
class="px-4 py-2 text-sm text-gray-600 hover:text-gray-800 border border-gray-300 rounded-lg transition-colors">
|
|
243
|
+
取消
|
|
244
|
+
</button>
|
|
245
|
+
<button id="deleteConfirmBtn"
|
|
246
|
+
class="px-4 py-2 text-sm text-white bg-red-600 hover:bg-red-700 rounded-lg transition-colors">
|
|
247
|
+
删除
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
</div>
|
|
252
|
+
</div>
|
|
253
|
+
|
|
254
|
+
<!-- Toast -->
|
|
255
|
+
<div id="toast" class="fixed top-4 right-4 bg-green-600 text-white px-4 py-2 rounded-lg shadow-lg text-sm hidden transition-all z-50"></div>
|
|
256
|
+
|
|
257
|
+
<script>
|
|
258
|
+
// ========== 状态 ==========
|
|
259
|
+
let currentPage = 1;
|
|
260
|
+
let totalPages = 1;
|
|
261
|
+
let totalRecords = 0;
|
|
262
|
+
let currentKeyword = '';
|
|
263
|
+
let currentDetailId = '';
|
|
264
|
+
let deletingId = '';
|
|
265
|
+
|
|
266
|
+
// ========== DOM ==========
|
|
267
|
+
const historyContainer = document.getElementById('historyContainer');
|
|
268
|
+
const emptyState = document.getElementById('emptyState');
|
|
269
|
+
const totalCount = document.getElementById('totalCount');
|
|
270
|
+
const pagination = document.getElementById('pagination');
|
|
271
|
+
const pageInfo = document.getElementById('pageInfo');
|
|
272
|
+
const prevPageBtn = document.getElementById('prevPageBtn');
|
|
273
|
+
const nextPageBtn = document.getElementById('nextPageBtn');
|
|
274
|
+
const searchInput = document.getElementById('searchInput');
|
|
275
|
+
|
|
276
|
+
const detailModal = document.getElementById('detailModal');
|
|
277
|
+
const editModal = document.getElementById('editModal');
|
|
278
|
+
const deleteModal = document.getElementById('deleteModal');
|
|
279
|
+
const toast = document.getElementById('toast');
|
|
280
|
+
|
|
281
|
+
// ========== 工具函数 ==========
|
|
282
|
+
function showToast(msg, isError = false) {
|
|
283
|
+
toast.textContent = msg;
|
|
284
|
+
toast.className = `fixed top-4 right-4 ${isError ? 'bg-red-600' : 'bg-green-600'} text-white px-4 py-2 rounded-lg shadow-lg text-sm transition-all z-50`;
|
|
285
|
+
setTimeout(() => { toast.classList.add('hidden'); }, 2000);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function formatDateTime(isoStr) {
|
|
289
|
+
if (!isoStr) return '-';
|
|
290
|
+
try {
|
|
291
|
+
const d = new Date(isoStr);
|
|
292
|
+
const year = d.getFullYear();
|
|
293
|
+
const month = String(d.getMonth() + 1).padStart(2, '0');
|
|
294
|
+
const day = String(d.getDate()).padStart(2, '0');
|
|
295
|
+
const hour = String(d.getHours()).padStart(2, '0');
|
|
296
|
+
const minute = String(d.getMinutes()).padStart(2, '0');
|
|
297
|
+
const second = String(d.getSeconds()).padStart(2, '0');
|
|
298
|
+
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
|
|
299
|
+
} catch {
|
|
300
|
+
return isoStr;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function truncateText(text, maxLen = 80) {
|
|
305
|
+
if (!text) return '';
|
|
306
|
+
if (text.length <= maxLen) return text;
|
|
307
|
+
return text.substring(0, maxLen) + '...';
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ========== 加载历史记录 ==========
|
|
311
|
+
async function loadHistory(page = 1, keyword = '') {
|
|
312
|
+
try {
|
|
313
|
+
const params = new URLSearchParams({ page: page, page_size: 50 });
|
|
314
|
+
if (keyword) params.set('keyword', keyword);
|
|
315
|
+
|
|
316
|
+
const resp = await fetch(`/api/history?${params}`);
|
|
317
|
+
const data = await resp.json();
|
|
318
|
+
|
|
319
|
+
totalRecords = data.total;
|
|
320
|
+
currentPage = data.page;
|
|
321
|
+
totalPages = data.total_pages;
|
|
322
|
+
|
|
323
|
+
totalCount.textContent = `共 ${totalRecords} 条`;
|
|
324
|
+
|
|
325
|
+
renderHistoryList(data.records);
|
|
326
|
+
renderPagination();
|
|
327
|
+
} catch (err) {
|
|
328
|
+
showToast('加载历史记录失败: ' + err.message, true);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ========== 渲染列表 ==========
|
|
333
|
+
function renderHistoryList(records) {
|
|
334
|
+
if (!records || records.length === 0) {
|
|
335
|
+
historyContainer.innerHTML = '';
|
|
336
|
+
emptyState.classList.remove('hidden');
|
|
337
|
+
pagination.classList.add('hidden');
|
|
338
|
+
return;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
emptyState.classList.add('hidden');
|
|
342
|
+
pagination.classList.remove('hidden');
|
|
343
|
+
|
|
344
|
+
historyContainer.innerHTML = records.map(record => {
|
|
345
|
+
const matchBadge = record.match_job_title
|
|
346
|
+
? '<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">已匹配</span>'
|
|
347
|
+
: '<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600">未匹配</span>';
|
|
348
|
+
|
|
349
|
+
const emailPreview = truncateText(record.email_content, 100);
|
|
350
|
+
|
|
351
|
+
return `
|
|
352
|
+
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 fade-in cursor-pointer hover:border-blue-300 transition-colors"
|
|
353
|
+
onclick="openDetail('${record.id}')">
|
|
354
|
+
<div class="flex items-start justify-between">
|
|
355
|
+
<div class="flex-1 min-w-0">
|
|
356
|
+
<div class="flex items-center space-x-2 mb-1">
|
|
357
|
+
<h3 class="text-sm font-semibold text-gray-800 truncate">${record.title || '未命名'}</h3>
|
|
358
|
+
${matchBadge}
|
|
359
|
+
</div>
|
|
360
|
+
<div class="flex items-center space-x-4 mt-1">
|
|
361
|
+
<span class="text-xs text-gray-400">
|
|
362
|
+
<svg class="w-3 h-3 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
363
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z"/>
|
|
364
|
+
</svg>
|
|
365
|
+
${record.candidate_name || '未知'}
|
|
366
|
+
</span>
|
|
367
|
+
<span class="text-xs text-gray-400">
|
|
368
|
+
<svg class="w-3 h-3 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
369
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
|
|
370
|
+
</svg>
|
|
371
|
+
${record.job_title || '未知'}
|
|
372
|
+
</span>
|
|
373
|
+
<span class="text-xs text-gray-400">
|
|
374
|
+
<svg class="w-3 h-3 inline-block mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
375
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
|
|
376
|
+
</svg>
|
|
377
|
+
${formatDateTime(record.created_at)}
|
|
378
|
+
</span>
|
|
379
|
+
</div>
|
|
380
|
+
<p class="text-xs text-gray-500 mt-2 line-clamp-2">${emailPreview}</p>
|
|
381
|
+
</div>
|
|
382
|
+
<div class="flex items-center space-x-1 ml-3 flex-shrink-0" onclick="event.stopPropagation()">
|
|
383
|
+
<button onclick="openDetail('${record.id}')"
|
|
384
|
+
class="text-gray-400 hover:text-blue-600 p-1.5 rounded-lg hover:bg-blue-50 transition-colors" title="查看详情">
|
|
385
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
386
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
|
|
387
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
|
|
388
|
+
</svg>
|
|
389
|
+
</button>
|
|
390
|
+
<button onclick="openEdit('${record.id}')"
|
|
391
|
+
class="text-gray-400 hover:text-blue-600 p-1.5 rounded-lg hover:bg-blue-50 transition-colors" title="编辑">
|
|
392
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
393
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"/>
|
|
394
|
+
</svg>
|
|
395
|
+
</button>
|
|
396
|
+
<button onclick="openDelete('${record.id}')"
|
|
397
|
+
class="text-gray-400 hover:text-red-600 p-1.5 rounded-lg hover:bg-red-50 transition-colors" title="删除">
|
|
398
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
399
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"/>
|
|
400
|
+
</svg>
|
|
401
|
+
</button>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</div>`;
|
|
405
|
+
}).join('');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// ========== 渲染分页 ==========
|
|
409
|
+
function renderPagination() {
|
|
410
|
+
pageInfo.textContent = `第 ${currentPage} 页 / 共 ${totalPages} 页`;
|
|
411
|
+
prevPageBtn.disabled = currentPage <= 1;
|
|
412
|
+
nextPageBtn.disabled = currentPage >= totalPages;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// ========== 分页事件 ==========
|
|
416
|
+
prevPageBtn.addEventListener('click', () => {
|
|
417
|
+
if (currentPage > 1) {
|
|
418
|
+
loadHistory(currentPage - 1, currentKeyword);
|
|
419
|
+
}
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
nextPageBtn.addEventListener('click', () => {
|
|
423
|
+
if (currentPage < totalPages) {
|
|
424
|
+
loadHistory(currentPage + 1, currentKeyword);
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// ========== 搜索 ==========
|
|
429
|
+
document.getElementById('searchBtn').addEventListener('click', () => {
|
|
430
|
+
currentKeyword = searchInput.value.trim();
|
|
431
|
+
currentPage = 1;
|
|
432
|
+
loadHistory(currentPage, currentKeyword);
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
searchInput.addEventListener('keydown', (e) => {
|
|
436
|
+
if (e.key === 'Enter') {
|
|
437
|
+
currentKeyword = searchInput.value.trim();
|
|
438
|
+
currentPage = 1;
|
|
439
|
+
loadHistory(currentPage, currentKeyword);
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
document.getElementById('clearSearchBtn').addEventListener('click', () => {
|
|
444
|
+
searchInput.value = '';
|
|
445
|
+
currentKeyword = '';
|
|
446
|
+
currentPage = 1;
|
|
447
|
+
loadHistory(currentPage, '');
|
|
448
|
+
});
|
|
449
|
+
|
|
450
|
+
// ========== 详情模态框 ==========
|
|
451
|
+
let currentDetailData = null;
|
|
452
|
+
|
|
453
|
+
async function openDetail(recordId) {
|
|
454
|
+
try {
|
|
455
|
+
const resp = await fetch(`/api/history/${recordId}`);
|
|
456
|
+
if (!resp.ok) {
|
|
457
|
+
showToast('获取详情失败', true);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
currentDetailData = await resp.json();
|
|
461
|
+
currentDetailId = recordId;
|
|
462
|
+
|
|
463
|
+
document.getElementById('detailTitle').textContent = currentDetailData.title || '未命名';
|
|
464
|
+
document.getElementById('detailMatchBadge').innerHTML = currentDetailData.match_job_title
|
|
465
|
+
? '<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">已匹配</span>'
|
|
466
|
+
: '<span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 text-gray-600">未匹配</span>';
|
|
467
|
+
document.getElementById('detailCandidateName').textContent = currentDetailData.candidate_name || '未知';
|
|
468
|
+
document.getElementById('detailJobTitle').textContent = currentDetailData.job_title || '未知';
|
|
469
|
+
document.getElementById('detailCreatedAt').textContent = formatDateTime(currentDetailData.created_at);
|
|
470
|
+
document.getElementById('detailUpdatedAt').textContent = formatDateTime(currentDetailData.updated_at);
|
|
471
|
+
document.getElementById('detailEmailContent').textContent = currentDetailData.email_content || '';
|
|
472
|
+
document.getElementById('detailResumeText').textContent = currentDetailData.resume_text || '无';
|
|
473
|
+
|
|
474
|
+
detailModal.classList.remove('hidden');
|
|
475
|
+
} catch (err) {
|
|
476
|
+
showToast('获取详情失败: ' + err.message, true);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function closeDetailModal() {
|
|
481
|
+
detailModal.classList.add('hidden');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
document.getElementById('detailCloseBtn').addEventListener('click', closeDetailModal);
|
|
485
|
+
document.getElementById('detailCloseBtn2').addEventListener('click', closeDetailModal);
|
|
486
|
+
document.getElementById('detailModalOverlay').addEventListener('click', closeDetailModal);
|
|
487
|
+
|
|
488
|
+
// 复制邮件
|
|
489
|
+
document.getElementById('detailCopyBtn').addEventListener('click', () => {
|
|
490
|
+
if (currentDetailData && currentDetailData.email_content) {
|
|
491
|
+
navigator.clipboard.writeText(currentDetailData.email_content).then(() => {
|
|
492
|
+
const btn = document.getElementById('detailCopyBtn');
|
|
493
|
+
btn.textContent = '已复制!';
|
|
494
|
+
setTimeout(() => { btn.textContent = '复制邮件'; }, 1500);
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
// 详情中点击编辑
|
|
500
|
+
document.getElementById('detailEditBtn').addEventListener('click', () => {
|
|
501
|
+
closeDetailModal();
|
|
502
|
+
openEdit(currentDetailId);
|
|
503
|
+
});
|
|
504
|
+
|
|
505
|
+
// 详情中点击删除
|
|
506
|
+
document.getElementById('detailDeleteBtn').addEventListener('click', () => {
|
|
507
|
+
closeDetailModal();
|
|
508
|
+
openDelete(currentDetailId);
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
// ========== 编辑模态框 ==========
|
|
512
|
+
async function openEdit(recordId) {
|
|
513
|
+
try {
|
|
514
|
+
const resp = await fetch(`/api/history/${recordId}`);
|
|
515
|
+
if (!resp.ok) {
|
|
516
|
+
showToast('获取记录失败', true);
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
const data = await resp.json();
|
|
520
|
+
currentDetailId = recordId;
|
|
521
|
+
|
|
522
|
+
document.getElementById('editCandidateName').value = data.candidate_name || '';
|
|
523
|
+
document.getElementById('editJobTitle').value = data.job_title || '';
|
|
524
|
+
document.getElementById('editEmailContent').value = data.email_content || '';
|
|
525
|
+
|
|
526
|
+
editModal.classList.remove('hidden');
|
|
527
|
+
} catch (err) {
|
|
528
|
+
showToast('获取记录失败: ' + err.message, true);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function closeEditModal() {
|
|
533
|
+
editModal.classList.add('hidden');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
document.getElementById('editCancelBtn').addEventListener('click', closeEditModal);
|
|
537
|
+
document.getElementById('editModalOverlay').addEventListener('click', closeEditModal);
|
|
538
|
+
|
|
539
|
+
document.getElementById('editSaveBtn').addEventListener('click', async () => {
|
|
540
|
+
const candidateName = document.getElementById('editCandidateName').value.trim();
|
|
541
|
+
const jobTitle = document.getElementById('editJobTitle').value.trim();
|
|
542
|
+
const emailContent = document.getElementById('editEmailContent').value.trim();
|
|
543
|
+
|
|
544
|
+
if (!emailContent) {
|
|
545
|
+
showToast('邮件内容不能为空', true);
|
|
546
|
+
return;
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
try {
|
|
550
|
+
const resp = await fetch(`/api/history/${currentDetailId}`, {
|
|
551
|
+
method: 'PUT',
|
|
552
|
+
headers: { 'Content-Type': 'application/json' },
|
|
553
|
+
body: JSON.stringify({
|
|
554
|
+
candidate_name: candidateName,
|
|
555
|
+
job_title: jobTitle,
|
|
556
|
+
email_content: emailContent,
|
|
557
|
+
}),
|
|
558
|
+
});
|
|
559
|
+
|
|
560
|
+
if (!resp.ok) {
|
|
561
|
+
const data = await resp.json();
|
|
562
|
+
showToast(data.error || '更新失败', true);
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
closeEditModal();
|
|
567
|
+
showToast('已更新');
|
|
568
|
+
loadHistory(currentPage, currentKeyword);
|
|
569
|
+
} catch (err) {
|
|
570
|
+
showToast('更新失败: ' + err.message, true);
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
|
|
574
|
+
// ========== 删除弹窗 ==========
|
|
575
|
+
function openDelete(recordId) {
|
|
576
|
+
deletingId = recordId;
|
|
577
|
+
deleteModal.classList.remove('hidden');
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
function closeDeleteModal() {
|
|
581
|
+
deleteModal.classList.add('hidden');
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
document.getElementById('deleteCancelBtn').addEventListener('click', closeDeleteModal);
|
|
585
|
+
document.getElementById('deleteModalOverlay').addEventListener('click', closeDeleteModal);
|
|
586
|
+
|
|
587
|
+
document.getElementById('deleteConfirmBtn').addEventListener('click', async () => {
|
|
588
|
+
try {
|
|
589
|
+
const resp = await fetch(`/api/history/${deletingId}`, {
|
|
590
|
+
method: 'DELETE',
|
|
591
|
+
});
|
|
592
|
+
|
|
593
|
+
if (!resp.ok) {
|
|
594
|
+
const data = await resp.json();
|
|
595
|
+
showToast(data.error || '删除失败', true);
|
|
596
|
+
return;
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
closeDeleteModal();
|
|
600
|
+
showToast('已删除');
|
|
601
|
+
loadHistory(currentPage, currentKeyword);
|
|
602
|
+
} catch (err) {
|
|
603
|
+
showToast('删除失败: ' + err.message, true);
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
// ========== 初始化 ==========
|
|
608
|
+
loadHistory();
|
|
609
|
+
</script>
|
|
610
|
+
</body>
|
|
611
|
+
</html>
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
<div class="flex space-x-4">
|
|
42
42
|
<a href="/" class="text-blue-600 font-medium text-sm">生成邮件</a>
|
|
43
43
|
<a href="/jobs" class="text-gray-500 hover:text-gray-700 text-sm">职位管理</a>
|
|
44
|
+
<a href="/history" class="text-gray-500 hover:text-gray-700 text-sm">历史消息</a>
|
|
44
45
|
<a href="/config" class="text-gray-500 hover:text-gray-700 text-sm">邮件模板配置</a>
|
|
45
46
|
</div>
|
|
46
47
|
</div>
|
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
<div class="flex space-x-4">
|
|
26
26
|
<a href="/" class="text-gray-500 hover:text-gray-700 text-sm">生成邮件</a>
|
|
27
27
|
<a href="/jobs" class="text-blue-600 font-medium text-sm">职位管理</a>
|
|
28
|
+
<a href="/history" class="text-gray-500 hover:text-gray-700 text-sm">历史消息</a>
|
|
28
29
|
<a href="/config" class="text-gray-500 hover:text-gray-700 text-sm">邮件模板配置</a>
|
|
29
30
|
</div>
|
|
30
31
|
</div>
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: cold-msg
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: 招聘冷邮件智能生成工具 - 基于候选人简历文本自动生成 Cold Email
|
|
5
5
|
Author-email: Chandler <275737875@qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -12,7 +12,6 @@ Classifier: Programming Language :: Python :: 3
|
|
|
12
12
|
Classifier: Programming Language :: Python :: 3.10
|
|
13
13
|
Classifier: Programming Language :: Python :: 3.11
|
|
14
14
|
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
-
Classifier: Topic :: Communications :: Email
|
|
16
15
|
Requires-Python: >=3.10
|
|
17
16
|
Description-Content-Type: text/markdown
|
|
18
17
|
Requires-Dist: llmdog>=0.1.0
|
|
@@ -45,13 +44,6 @@ Requires-Dist: mypy>=1.0; extra == "dev"
|
|
|
45
44
|
pip install cold-msg
|
|
46
45
|
```
|
|
47
46
|
|
|
48
|
-
或从源码安装:
|
|
49
|
-
|
|
50
|
-
```bash
|
|
51
|
-
git clone https://github.com/example/cold-msg.git
|
|
52
|
-
cd cold-msg
|
|
53
|
-
pip install -e .
|
|
54
|
-
```
|
|
55
47
|
|
|
56
48
|
## 快速开始
|
|
57
49
|
|
|
@@ -223,21 +215,7 @@ cold_msg/
|
|
|
223
215
|
3. **`config.yaml`** — 项目根目录
|
|
224
216
|
4. **内置默认值**
|
|
225
217
|
|
|
226
|
-
## 开发
|
|
227
|
-
|
|
228
|
-
```bash
|
|
229
|
-
# 安装开发依赖
|
|
230
|
-
pip install -e ".[dev]"
|
|
231
|
-
|
|
232
|
-
# 代码检查
|
|
233
|
-
ruff check .
|
|
234
218
|
|
|
235
|
-
# 类型检查
|
|
236
|
-
mypy cold_msg
|
|
237
|
-
|
|
238
|
-
# 运行测试
|
|
239
|
-
pytest
|
|
240
|
-
```
|
|
241
219
|
|
|
242
220
|
## License
|
|
243
221
|
|
|
@@ -11,10 +11,12 @@ cold_msg.egg-info/top_level.txt
|
|
|
11
11
|
cold_msg/core/__init__.py
|
|
12
12
|
cold_msg/core/config.py
|
|
13
13
|
cold_msg/core/generator.py
|
|
14
|
+
cold_msg/core/history.py
|
|
14
15
|
cold_msg/core/llm.py
|
|
15
16
|
cold_msg/core/prompt.py
|
|
16
17
|
cold_msg/web/__init__.py
|
|
17
18
|
cold_msg/web/app.py
|
|
18
19
|
cold_msg/web/templates/config.html
|
|
20
|
+
cold_msg/web/templates/history.html
|
|
19
21
|
cold_msg/web/templates/index.html
|
|
20
22
|
cold_msg/web/templates/jobs.html
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "cold-msg"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "招聘冷邮件智能生成工具 - 基于候选人简历文本自动生成 Cold Email"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -21,7 +21,6 @@ classifiers = [
|
|
|
21
21
|
"Programming Language :: Python :: 3.10",
|
|
22
22
|
"Programming Language :: Python :: 3.11",
|
|
23
23
|
"Programming Language :: Python :: 3.12",
|
|
24
|
-
"Topic :: Communications :: Email",
|
|
25
24
|
]
|
|
26
25
|
|
|
27
26
|
dependencies = [
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|