cold-msg 0.1.0__py3-none-any.whl
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/__init__.py +7 -0
- cold_msg/cli.py +282 -0
- cold_msg/core/__init__.py +8 -0
- cold_msg/core/config.py +215 -0
- cold_msg/core/generator.py +129 -0
- cold_msg/core/llm.py +78 -0
- cold_msg/core/prompt.py +128 -0
- cold_msg/web/__init__.py +1 -0
- cold_msg/web/app.py +279 -0
- cold_msg/web/templates/config.html +357 -0
- cold_msg/web/templates/index.html +276 -0
- cold_msg/web/templates/jobs.html +518 -0
- cold_msg-0.1.0.dist-info/METADATA +244 -0
- cold_msg-0.1.0.dist-info/RECORD +17 -0
- cold_msg-0.1.0.dist-info/WHEEL +5 -0
- cold_msg-0.1.0.dist-info/entry_points.txt +2 -0
- cold_msg-0.1.0.dist-info/top_level.txt +1 -0
cold_msg/core/llm.py
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"""LLM 调用模块
|
|
2
|
+
|
|
3
|
+
封装 llmdog 库的调用逻辑,提供重试、响应清理、JSON 解析等功能。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import json
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Optional
|
|
9
|
+
|
|
10
|
+
from llmdog import chat as llmdog_chat
|
|
11
|
+
from llmdog.config import load_config as llmdog_load_config
|
|
12
|
+
|
|
13
|
+
from cold_msg.core.config import ColdMsgConfig
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def _clean_llm_response(response: str) -> str:
|
|
19
|
+
"""清理 LLM 返回的响应,移除 markdown 代码块标记"""
|
|
20
|
+
cleaned = response.strip()
|
|
21
|
+
if cleaned.startswith("```json"):
|
|
22
|
+
cleaned = cleaned[7:].strip()
|
|
23
|
+
if cleaned.startswith("```"):
|
|
24
|
+
cleaned = cleaned[3:].strip()
|
|
25
|
+
if cleaned.endswith("```"):
|
|
26
|
+
cleaned = cleaned[:-3].strip()
|
|
27
|
+
return cleaned
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _parse_json_safely(json_str: str) -> dict:
|
|
31
|
+
"""安全解析 JSON 字符串"""
|
|
32
|
+
try:
|
|
33
|
+
return json.loads(json_str)
|
|
34
|
+
except json.JSONDecodeError as e:
|
|
35
|
+
logger.error("JSON 解析失败: %s", e)
|
|
36
|
+
logger.debug("原始内容: %s...", json_str[:200])
|
|
37
|
+
return {}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def call_llm(
|
|
41
|
+
prompt: str,
|
|
42
|
+
cfg: ColdMsgConfig,
|
|
43
|
+
model: Optional[str] = None,
|
|
44
|
+
) -> dict:
|
|
45
|
+
"""调用 LLM 生成邮件内容
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
prompt: 完整的 Prompt 字符串
|
|
49
|
+
cfg: ColdMsg 配置实例
|
|
50
|
+
model: 指定模型名称,为 None 时使用配置中的模型
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
解析后的字典,包含 match_job_title, recommend_title, email_content 等字段。
|
|
54
|
+
调用失败时返回空字典。
|
|
55
|
+
"""
|
|
56
|
+
model_name = model or cfg.llm_model
|
|
57
|
+
|
|
58
|
+
logger.info("正在调用 LLM (model=%s)...", model_name)
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
raw_response = llmdog_chat(prompt)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
logger.error("LLM 调用失败: %s", e)
|
|
64
|
+
return {}
|
|
65
|
+
|
|
66
|
+
if not raw_response or not isinstance(raw_response, str):
|
|
67
|
+
logger.warning("LLM 返回无效响应")
|
|
68
|
+
return {}
|
|
69
|
+
|
|
70
|
+
logger.info("LLM 调用成功,返回长度: %d", len(raw_response))
|
|
71
|
+
|
|
72
|
+
cleaned = _clean_llm_response(raw_response)
|
|
73
|
+
parsed = _parse_json_safely(cleaned)
|
|
74
|
+
|
|
75
|
+
if not parsed:
|
|
76
|
+
logger.error("无法解析 LLM 返回的 JSON")
|
|
77
|
+
|
|
78
|
+
return parsed
|
cold_msg/core/prompt.py
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"""提示词构建模块
|
|
2
|
+
|
|
3
|
+
负责将候选人简历文本、职位列表、邮件模板组合为完整的 LLM Prompt。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from cold_msg.core.config import ColdMsgConfig, JobPosition, EmailTemplate
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
DEFAULT_SYSTEM_PROMPT = """你是招聘邮件撰写资深专家,以上是候选人的信息,请仔细阅读,先根据末尾的职位列表,评估他适合哪个岗位,然后写一份Email的Sales Pitch给他,如果评估不适合列表中的任何职位,match_job_title返回false,job_role和email_content都为空,如果评估适合列表中的职位,则给出具体的Email内容,Email的示例如下:JSON格式返回,key包括:{"match_job_title": true/false, "recommend_title": "","email_content": ""}"""
|
|
10
|
+
|
|
11
|
+
DEFAULT_EMAIL_EXAMPLE = """您好{FullName},
|
|
12
|
+
我来自{TeamName},很冒昧的给您发邮件。通过Linkedin了解到您,了解到您在XXX, XXX等方向有丰富经验。
|
|
13
|
+
|
|
14
|
+
我负责{CompanyDesc}招聘,非常希望邀请到您这样的人才在此向您推荐"{JobTitle}"的职位。负责带领技术团队攻克更多XXX,XXX技术难题,进一步提升您在行业内的影响力。
|
|
15
|
+
|
|
16
|
+
职位地点: {Location}
|
|
17
|
+
|
|
18
|
+
如果您有兴趣具体了解的话,可以留个电话或者微信,给您详细介绍一下,期待与您的沟通~
|
|
19
|
+
|
|
20
|
+
祝颂商祺
|
|
21
|
+
{SenderName}"""
|
|
22
|
+
|
|
23
|
+
DEFAULT_CONTENT_REQUIREMENTS = """# 请根据候选人技术背景,撰写一封直接可用的中文销售推介邮件,要求如下:
|
|
24
|
+
|
|
25
|
+
1. 匹配评估:结合候选人技术方向与职位列表中的具体岗位(含JobTitle和Location),评估匹配度, 必须要写Location。
|
|
26
|
+
|
|
27
|
+
2. 内容要求:
|
|
28
|
+
- 结构清晰:开头简要说明来意,主体突出技术匹配点,结尾自然引导联系。
|
|
29
|
+
- 简洁流畅:语言自然、口语化,避免冗余和套话。
|
|
30
|
+
- 态度诚恳:不浮夸,不使用"让我印象深刻""很欣赏您"等表达。
|
|
31
|
+
- 不含占位符:输出为完整、可直接发送的邮件正文。
|
|
32
|
+
|
|
33
|
+
3. 语言风格:
|
|
34
|
+
- 使用中文,简洁自然,符合职场沟通习惯。
|
|
35
|
+
- 避免"体现了技术实力""由于网络原因无法解析链接"等无效或技术性表述。
|
|
36
|
+
|
|
37
|
+
4. 结尾要求:
|
|
38
|
+
- 在邮件末尾自然添加一句联系方式索要语,例如:"如果您有兴趣进一步了解,欢迎留下电话或微信,我将为您详细介绍。"
|
|
39
|
+
|
|
40
|
+
5. 禁用词项(确保不出现):
|
|
41
|
+
- "让我印象深刻"
|
|
42
|
+
- "很欣赏您"
|
|
43
|
+
- "体现了您技术实力"
|
|
44
|
+
- "由于网络原因,我无法成功解析您提供的个人主页链接"
|
|
45
|
+
- 其他主观性、冗余性表达
|
|
46
|
+
|
|
47
|
+
请输出一封完整、可直接发送的中文销售推介邮件。"""
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _format_jobs_section(jobs: list[JobPosition]) -> str:
|
|
51
|
+
"""将职位列表格式化为 Prompt 中的文本段落"""
|
|
52
|
+
lines = ["职位列表:\n"]
|
|
53
|
+
for i, job in enumerate(jobs, 1):
|
|
54
|
+
lines.append(
|
|
55
|
+
f"{i}. JobTitle:{job.job_title},"
|
|
56
|
+
f"Qualification:{job.qualification},"
|
|
57
|
+
f"Location:{job.location}"
|
|
58
|
+
)
|
|
59
|
+
return "\n".join(lines)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _format_email_example(template: EmailTemplate) -> str:
|
|
63
|
+
"""格式化邮件示例模板
|
|
64
|
+
|
|
65
|
+
使用 EmailTemplate 的各字段拼接出完整的邮件示例,
|
|
66
|
+
保留 {FullName}, {JobTitle}, {Location} 作为 LLM 需要填充的占位符。
|
|
67
|
+
"""
|
|
68
|
+
parts = [
|
|
69
|
+
template.greeting,
|
|
70
|
+
template.intro,
|
|
71
|
+
"",
|
|
72
|
+
template.body,
|
|
73
|
+
"",
|
|
74
|
+
template.location_line,
|
|
75
|
+
"",
|
|
76
|
+
template.closing,
|
|
77
|
+
"",
|
|
78
|
+
template.sign_off,
|
|
79
|
+
template.signature,
|
|
80
|
+
]
|
|
81
|
+
raw = "\n".join(parts)
|
|
82
|
+
|
|
83
|
+
return raw.format(
|
|
84
|
+
FullName="{FullName}",
|
|
85
|
+
TeamName=template.team_name,
|
|
86
|
+
CompanyDesc=template.company_desc,
|
|
87
|
+
JobTitle="{JobTitle}",
|
|
88
|
+
Location="{Location}",
|
|
89
|
+
SenderName=template.sender_name,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_prompt(
|
|
94
|
+
resume_text: str,
|
|
95
|
+
jobs: list[JobPosition],
|
|
96
|
+
email_template: EmailTemplate | None = None,
|
|
97
|
+
custom_prompt: str | None = None,
|
|
98
|
+
) -> str:
|
|
99
|
+
"""构建完整的 LLM Prompt
|
|
100
|
+
|
|
101
|
+
Args:
|
|
102
|
+
resume_text: 候选人简历文本
|
|
103
|
+
jobs: 职位列表
|
|
104
|
+
email_template: 邮件模板配置,为 None 时使用默认值
|
|
105
|
+
custom_prompt: 自定义提示词,为 None 时使用默认值
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
完整的 Prompt 字符串
|
|
109
|
+
"""
|
|
110
|
+
if email_template is None:
|
|
111
|
+
email_template = EmailTemplate()
|
|
112
|
+
|
|
113
|
+
parts = []
|
|
114
|
+
|
|
115
|
+
parts.append(f"#### 候选人信息\n\n{resume_text}\n")
|
|
116
|
+
|
|
117
|
+
if custom_prompt:
|
|
118
|
+
parts.append(f"####\n{custom_prompt}\n")
|
|
119
|
+
else:
|
|
120
|
+
parts.append(f"####\n{DEFAULT_SYSTEM_PROMPT}\n")
|
|
121
|
+
|
|
122
|
+
parts.append(f"####\n{_format_email_example(email_template)}\n")
|
|
123
|
+
|
|
124
|
+
parts.append(DEFAULT_CONTENT_REQUIREMENTS)
|
|
125
|
+
|
|
126
|
+
parts.append(f"\n# 职位列表如下: \n\n```\n{_format_jobs_section(jobs)}\n```\n")
|
|
127
|
+
|
|
128
|
+
return "\n".join(parts)
|
cold_msg/web/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Web 模块"""
|
cold_msg/web/app.py
ADDED
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"""Flask Web 应用
|
|
2
|
+
|
|
3
|
+
提供 Web 界面用于:
|
|
4
|
+
- 输入候选人简历文本
|
|
5
|
+
- 配置职位列表
|
|
6
|
+
- 配置邮件模板
|
|
7
|
+
- 一键生成冷邮件
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import yaml
|
|
12
|
+
from flask import Flask, render_template, request, jsonify
|
|
13
|
+
|
|
14
|
+
from cold_msg.core.config import ColdMsgConfig, JobPosition, EmailTemplate, load_config, save_config
|
|
15
|
+
from cold_msg.core.generator import generate_cold_email
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_app(cfg: ColdMsgConfig | None = None) -> Flask:
|
|
19
|
+
"""创建 Flask 应用
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
cfg: 配置实例,为 None 时自动加载
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
Flask 应用实例
|
|
26
|
+
"""
|
|
27
|
+
app = Flask(
|
|
28
|
+
__name__,
|
|
29
|
+
template_folder="templates",
|
|
30
|
+
static_folder="static",
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
if cfg is None:
|
|
34
|
+
cfg = load_config()
|
|
35
|
+
|
|
36
|
+
app.config["COLDMSG_CFG"] = cfg
|
|
37
|
+
|
|
38
|
+
@app.route("/")
|
|
39
|
+
def index():
|
|
40
|
+
"""主页 - 邮件生成"""
|
|
41
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
42
|
+
return render_template("index.html", cfg=cfg)
|
|
43
|
+
|
|
44
|
+
@app.route("/config")
|
|
45
|
+
def config_page():
|
|
46
|
+
"""配置页面"""
|
|
47
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
48
|
+
return render_template("config.html", cfg=cfg)
|
|
49
|
+
|
|
50
|
+
@app.route("/jobs")
|
|
51
|
+
def jobs_page():
|
|
52
|
+
"""职位管理页面"""
|
|
53
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
54
|
+
return render_template("jobs.html", cfg=cfg)
|
|
55
|
+
|
|
56
|
+
@app.route("/api/generate", methods=["POST"])
|
|
57
|
+
def api_generate():
|
|
58
|
+
"""生成冷邮件 API"""
|
|
59
|
+
data = request.get_json()
|
|
60
|
+
if not data:
|
|
61
|
+
return jsonify({"error": "请求数据为空"}), 400
|
|
62
|
+
|
|
63
|
+
resume_text = data.get("resume_text", "").strip()
|
|
64
|
+
if not resume_text:
|
|
65
|
+
return jsonify({"error": "请输入候选人简历文本"}), 400
|
|
66
|
+
|
|
67
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
68
|
+
|
|
69
|
+
jobs_data = data.get("jobs")
|
|
70
|
+
jobs = None
|
|
71
|
+
if jobs_data:
|
|
72
|
+
jobs = [
|
|
73
|
+
JobPosition(
|
|
74
|
+
job_title=j.get("job_title", ""),
|
|
75
|
+
qualification=j.get("qualification", ""),
|
|
76
|
+
location=j.get("location", ""),
|
|
77
|
+
)
|
|
78
|
+
for j in jobs_data
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
tpl_data = data.get("email_template")
|
|
82
|
+
email_template = None
|
|
83
|
+
if tpl_data:
|
|
84
|
+
email_template = EmailTemplate(**tpl_data)
|
|
85
|
+
|
|
86
|
+
model = data.get("model")
|
|
87
|
+
|
|
88
|
+
try:
|
|
89
|
+
result = generate_cold_email(
|
|
90
|
+
resume_text=resume_text,
|
|
91
|
+
cfg=cfg,
|
|
92
|
+
jobs=jobs,
|
|
93
|
+
email_template=email_template,
|
|
94
|
+
model=model,
|
|
95
|
+
)
|
|
96
|
+
return jsonify(result.to_dict())
|
|
97
|
+
except Exception as e:
|
|
98
|
+
return jsonify({"error": f"生成失败: {str(e)}"}), 500
|
|
99
|
+
|
|
100
|
+
@app.route("/api/config", methods=["GET"])
|
|
101
|
+
def api_get_config():
|
|
102
|
+
"""获取当前配置"""
|
|
103
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
104
|
+
return jsonify(cfg.to_dict())
|
|
105
|
+
|
|
106
|
+
@app.route("/api/config", methods=["POST"])
|
|
107
|
+
def api_save_config():
|
|
108
|
+
"""保存配置"""
|
|
109
|
+
data = request.get_json()
|
|
110
|
+
if not data:
|
|
111
|
+
return jsonify({"error": "请求数据为空"}), 400
|
|
112
|
+
|
|
113
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
114
|
+
|
|
115
|
+
for key in ["llm_model"]:
|
|
116
|
+
if key in data:
|
|
117
|
+
setattr(cfg, key, data[key])
|
|
118
|
+
|
|
119
|
+
if "jobs" in data:
|
|
120
|
+
cfg.jobs = [
|
|
121
|
+
JobPosition(
|
|
122
|
+
job_title=j.get("job_title", ""),
|
|
123
|
+
qualification=j.get("qualification", ""),
|
|
124
|
+
location=j.get("location", ""),
|
|
125
|
+
)
|
|
126
|
+
for j in data["jobs"]
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
if "email_template" in data:
|
|
130
|
+
tpl_data = data["email_template"]
|
|
131
|
+
current = cfg.email_template.to_dict()
|
|
132
|
+
current.update({k: v for k, v in tpl_data.items() if v is not None})
|
|
133
|
+
cfg.email_template = EmailTemplate(**current)
|
|
134
|
+
|
|
135
|
+
try:
|
|
136
|
+
path = save_config(cfg)
|
|
137
|
+
return jsonify({"message": f"配置已保存: {path}"})
|
|
138
|
+
except Exception as e:
|
|
139
|
+
return jsonify({"error": f"保存失败: {str(e)}"}), 500
|
|
140
|
+
|
|
141
|
+
@app.route("/api/jobs", methods=["GET"])
|
|
142
|
+
def api_get_jobs():
|
|
143
|
+
"""获取职位列表"""
|
|
144
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
145
|
+
return jsonify({"jobs": [j.to_dict() for j in cfg.jobs]})
|
|
146
|
+
|
|
147
|
+
@app.route("/api/jobs", methods=["POST"])
|
|
148
|
+
def api_update_jobs():
|
|
149
|
+
"""更新职位列表"""
|
|
150
|
+
data = request.get_json()
|
|
151
|
+
if not data or "jobs" not in data:
|
|
152
|
+
return jsonify({"error": "请求数据格式错误"}), 400
|
|
153
|
+
|
|
154
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
155
|
+
cfg.jobs = [
|
|
156
|
+
JobPosition(
|
|
157
|
+
job_title=j.get("job_title", ""),
|
|
158
|
+
qualification=j.get("qualification", ""),
|
|
159
|
+
location=j.get("location", ""),
|
|
160
|
+
)
|
|
161
|
+
for j in data["jobs"]
|
|
162
|
+
]
|
|
163
|
+
|
|
164
|
+
try:
|
|
165
|
+
save_config(cfg)
|
|
166
|
+
return jsonify({"message": f"已更新 {len(cfg.jobs)} 个职位"})
|
|
167
|
+
except Exception as e:
|
|
168
|
+
return jsonify({"error": f"保存失败: {str(e)}"}), 500
|
|
169
|
+
|
|
170
|
+
@app.route("/api/jobs/import-yaml", methods=["POST"])
|
|
171
|
+
def api_import_yaml():
|
|
172
|
+
"""从 YAML 文本导入职位列表(追加模式)"""
|
|
173
|
+
data = request.get_json()
|
|
174
|
+
if not data or "yaml_text" not in data:
|
|
175
|
+
return jsonify({"error": "请求数据格式错误,需要 yaml_text 字段"}), 400
|
|
176
|
+
|
|
177
|
+
yaml_text = data["yaml_text"]
|
|
178
|
+
try:
|
|
179
|
+
parsed = yaml.safe_load(yaml_text)
|
|
180
|
+
except yaml.YAMLError as e:
|
|
181
|
+
return jsonify({"error": f"YAML 解析失败: {str(e)}"}), 400
|
|
182
|
+
|
|
183
|
+
if isinstance(parsed, dict):
|
|
184
|
+
parsed = parsed.get("jobs", parsed.get("positions", []))
|
|
185
|
+
if not isinstance(parsed, list):
|
|
186
|
+
return jsonify({"error": "YAML 内容应为列表格式"}), 400
|
|
187
|
+
|
|
188
|
+
new_jobs = []
|
|
189
|
+
for j in parsed:
|
|
190
|
+
title = j.get("job_title", j.get("JobTitle", ""))
|
|
191
|
+
if not title or not str(title).strip():
|
|
192
|
+
continue
|
|
193
|
+
new_jobs.append(JobPosition(
|
|
194
|
+
job_title=str(title).strip(),
|
|
195
|
+
qualification=str(j.get("qualification", j.get("Qualification", ""))).strip(),
|
|
196
|
+
location=str(j.get("location", j.get("Location", ""))).strip(),
|
|
197
|
+
))
|
|
198
|
+
|
|
199
|
+
if not new_jobs:
|
|
200
|
+
return jsonify({"error": "未解析到有效职位(职位名称不能为空)"}), 400
|
|
201
|
+
|
|
202
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
203
|
+
cfg.jobs.extend(new_jobs)
|
|
204
|
+
|
|
205
|
+
try:
|
|
206
|
+
save_config(cfg)
|
|
207
|
+
return jsonify({
|
|
208
|
+
"message": f"已导入 {len(new_jobs)} 个职位",
|
|
209
|
+
"imported": len(new_jobs),
|
|
210
|
+
"total": len(cfg.jobs),
|
|
211
|
+
})
|
|
212
|
+
except Exception as e:
|
|
213
|
+
return jsonify({"error": f"保存失败: {str(e)}"}), 500
|
|
214
|
+
|
|
215
|
+
@app.route("/api/email-template", methods=["GET"])
|
|
216
|
+
def api_get_email_template():
|
|
217
|
+
"""获取邮件模板"""
|
|
218
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
219
|
+
return jsonify({"email_template": cfg.email_template.to_dict()})
|
|
220
|
+
|
|
221
|
+
@app.route("/api/email-template", methods=["POST"])
|
|
222
|
+
def api_update_email_template():
|
|
223
|
+
"""更新邮件模板"""
|
|
224
|
+
data = request.get_json()
|
|
225
|
+
if not data or "email_template" not in data:
|
|
226
|
+
return jsonify({"error": "请求数据格式错误,需要 email_template 字段"}), 400
|
|
227
|
+
|
|
228
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
229
|
+
tpl_data = data["email_template"]
|
|
230
|
+
|
|
231
|
+
current = cfg.email_template.to_dict()
|
|
232
|
+
current.update({k: v for k, v in tpl_data.items() if v is not None})
|
|
233
|
+
cfg.email_template = EmailTemplate(**current)
|
|
234
|
+
|
|
235
|
+
try:
|
|
236
|
+
save_config(cfg)
|
|
237
|
+
return jsonify({
|
|
238
|
+
"message": "邮件模板已更新",
|
|
239
|
+
"email_template": cfg.email_template.to_dict(),
|
|
240
|
+
})
|
|
241
|
+
except Exception as e:
|
|
242
|
+
return jsonify({"error": f"保存失败: {str(e)}"}), 500
|
|
243
|
+
|
|
244
|
+
@app.route("/api/email-template/preview", methods=["POST"])
|
|
245
|
+
def api_preview_email_template():
|
|
246
|
+
"""预览邮件模板(不保存)"""
|
|
247
|
+
data = request.get_json()
|
|
248
|
+
if not data:
|
|
249
|
+
return jsonify({"error": "请求数据为空"}), 400
|
|
250
|
+
|
|
251
|
+
tpl_data = data.get("email_template", {})
|
|
252
|
+
if not tpl_data:
|
|
253
|
+
cfg: ColdMsgConfig = app.config["COLDMSG_CFG"]
|
|
254
|
+
tpl_data = cfg.email_template.to_dict()
|
|
255
|
+
|
|
256
|
+
tpl = EmailTemplate(**tpl_data)
|
|
257
|
+
|
|
258
|
+
parts = [
|
|
259
|
+
tpl.greeting,
|
|
260
|
+
tpl.intro,
|
|
261
|
+
"",
|
|
262
|
+
tpl.body,
|
|
263
|
+
"",
|
|
264
|
+
tpl.location_line,
|
|
265
|
+
"",
|
|
266
|
+
tpl.closing,
|
|
267
|
+
"",
|
|
268
|
+
tpl.sign_off,
|
|
269
|
+
tpl.signature,
|
|
270
|
+
]
|
|
271
|
+
text = "\n".join(parts)
|
|
272
|
+
|
|
273
|
+
text = text.replace("{TeamName}", tpl.team_name)
|
|
274
|
+
text = text.replace("{CompanyDesc}", tpl.company_desc)
|
|
275
|
+
text = text.replace("{SenderName}", tpl.sender_name)
|
|
276
|
+
|
|
277
|
+
return jsonify({"preview": text})
|
|
278
|
+
|
|
279
|
+
return app
|