replyme 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.
- replyme/__init__.py +8 -0
- replyme/cli.py +251 -0
- replyme/config.py +299 -0
- replyme/core.py +133 -0
- replyme/logger.py +56 -0
- replyme/reader.py +110 -0
- replyme-0.1.0.dist-info/METADATA +722 -0
- replyme-0.1.0.dist-info/RECORD +10 -0
- replyme-0.1.0.dist-info/WHEEL +4 -0
- replyme-0.1.0.dist-info/entry_points.txt +2 -0
replyme/__init__.py
ADDED
replyme/cli.py
ADDED
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
"""replyme 命令行接口,基于 Typer 实现。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Annotated, Optional
|
|
9
|
+
|
|
10
|
+
# Windows 下确保 stdout 使用 UTF-8 编码,避免 emoji 等字符输出报错
|
|
11
|
+
if sys.platform == "win32":
|
|
12
|
+
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
|
13
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
14
|
+
try:
|
|
15
|
+
sys.stdout.reconfigure(encoding="utf-8", errors="replace")
|
|
16
|
+
except Exception:
|
|
17
|
+
pass
|
|
18
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
19
|
+
try:
|
|
20
|
+
sys.stderr.reconfigure(encoding="utf-8", errors="replace")
|
|
21
|
+
except Exception:
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
import typer
|
|
25
|
+
from rich.console import Console
|
|
26
|
+
from rich.panel import Panel
|
|
27
|
+
from rich.text import Text
|
|
28
|
+
from rich.table import Table
|
|
29
|
+
|
|
30
|
+
from replyme.config import (
|
|
31
|
+
EMAIL_STYLES,
|
|
32
|
+
IM_STYLES,
|
|
33
|
+
ReplymeConfig,
|
|
34
|
+
load_config,
|
|
35
|
+
save_config,
|
|
36
|
+
init_config,
|
|
37
|
+
get_config_dir,
|
|
38
|
+
)
|
|
39
|
+
from replyme.core import generate_series
|
|
40
|
+
from replyme.reader import read_files
|
|
41
|
+
from replyme.logger import save_follow_up_log, make_stem_from_topic
|
|
42
|
+
|
|
43
|
+
app = typer.Typer(
|
|
44
|
+
name="replyme",
|
|
45
|
+
help="有礼貌地生成一系列不重样的跟进消息,坚持不懈直到对方回复。",
|
|
46
|
+
add_completion=False,
|
|
47
|
+
no_args_is_help=True,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
console = Console()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
# ── init 命令 ───────────────────────────────────────────────
|
|
54
|
+
|
|
55
|
+
@app.command()
|
|
56
|
+
def init() -> None:
|
|
57
|
+
"""初始化本地配置目录和默认提示词文件。"""
|
|
58
|
+
config_dir = get_config_dir()
|
|
59
|
+
|
|
60
|
+
if config_dir.exists():
|
|
61
|
+
console.print(
|
|
62
|
+
f"[yellow]配置目录已存在:[/] {config_dir}\n"
|
|
63
|
+
f"[dim]如需重置,请先删除 {config_dir} 后重新运行 init。[/dim]"
|
|
64
|
+
)
|
|
65
|
+
cfg = load_config()
|
|
66
|
+
_show_config(cfg)
|
|
67
|
+
return
|
|
68
|
+
|
|
69
|
+
paths = init_config()
|
|
70
|
+
|
|
71
|
+
console.print(
|
|
72
|
+
Panel(
|
|
73
|
+
f"[bold green]初始化完成![/bold green]\n\n"
|
|
74
|
+
f"配置文件: {paths['config']}\n"
|
|
75
|
+
f"提示词文件: {paths['prompts']}\n\n"
|
|
76
|
+
f"[bold]修改提示词:[/]\n"
|
|
77
|
+
f" 编辑 {paths['prompts']} 即可自定义各场景/风格的系统提示词。\n"
|
|
78
|
+
f" 文件结构为 JSON,按 scene > style > prompt_text 层级组织。\n"
|
|
79
|
+
f" 修改后立即生效,无需重启。\n\n"
|
|
80
|
+
f"[bold]修改默认配置:[/]\n"
|
|
81
|
+
f" 编辑 {paths['config']} 可修改默认场景、风格等参数。",
|
|
82
|
+
title="replyme init",
|
|
83
|
+
border_style="green",
|
|
84
|
+
)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
# ── send 命令 ───────────────────────────────────────────────
|
|
89
|
+
|
|
90
|
+
@app.command()
|
|
91
|
+
def send(
|
|
92
|
+
topic: Annotated[
|
|
93
|
+
Optional[str],
|
|
94
|
+
typer.Argument(help="想要讨论的主题或问题(与 --input 二选一)"),
|
|
95
|
+
] = None,
|
|
96
|
+
input_files: Annotated[
|
|
97
|
+
Optional[list[Path]],
|
|
98
|
+
typer.Option(
|
|
99
|
+
"--input", "-i",
|
|
100
|
+
help="输入文件路径(支持 txt/json/md),可指定多个",
|
|
101
|
+
exists=True,
|
|
102
|
+
),
|
|
103
|
+
] = None,
|
|
104
|
+
scene: Annotated[
|
|
105
|
+
str,
|
|
106
|
+
typer.Option("--scene", "-s", help="场景: email(邮件) 或 im(即时通讯)"),
|
|
107
|
+
] = "im",
|
|
108
|
+
style: Annotated[
|
|
109
|
+
Optional[str],
|
|
110
|
+
typer.Option("--style", "-t", help="风格(不指定则使用配置文件或默认值)"),
|
|
111
|
+
] = None,
|
|
112
|
+
count: Annotated[
|
|
113
|
+
int,
|
|
114
|
+
typer.Option("--count", "-n", help="生成消息的数量", min=1, max=20),
|
|
115
|
+
] = 5,
|
|
116
|
+
model: Annotated[
|
|
117
|
+
Optional[str],
|
|
118
|
+
typer.Option("--model", "-m", help="指定 LLM 模型名称"),
|
|
119
|
+
] = None,
|
|
120
|
+
interval: Annotated[
|
|
121
|
+
int,
|
|
122
|
+
typer.Option("--interval", "--wait", help="每条消息之间的间隔秒数", min=0),
|
|
123
|
+
] = 0,
|
|
124
|
+
output_dir: Annotated[
|
|
125
|
+
Optional[Path],
|
|
126
|
+
typer.Option("--output", "-o", help="日志输出目录(默认 replyme/logs/)"),
|
|
127
|
+
] = None,
|
|
128
|
+
) -> None:
|
|
129
|
+
"""生成一系列有礼貌、不重样的跟进消息。"""
|
|
130
|
+
import time
|
|
131
|
+
|
|
132
|
+
# 验证输入
|
|
133
|
+
if not topic and not input_files:
|
|
134
|
+
console.print(
|
|
135
|
+
"[red]错误:[/] 请提供主题(topic)或输入文件(--input),至少需要一个。"
|
|
136
|
+
)
|
|
137
|
+
raise typer.Exit(code=1)
|
|
138
|
+
|
|
139
|
+
# 加载本地配置
|
|
140
|
+
cfg = load_config()
|
|
141
|
+
|
|
142
|
+
# 合并参数:命令行参数优先于配置文件
|
|
143
|
+
effective_scene = scene or cfg.scene
|
|
144
|
+
effective_style = style or cfg.style or _default_style(effective_scene)
|
|
145
|
+
effective_model = model or cfg.model
|
|
146
|
+
effective_count = count
|
|
147
|
+
effective_interval = interval or cfg.interval
|
|
148
|
+
|
|
149
|
+
# 验证场景
|
|
150
|
+
if effective_scene not in ("email", "im"):
|
|
151
|
+
console.print(f"[red]错误:[/] 不支持的场景 '{effective_scene}',请使用 email 或 im")
|
|
152
|
+
raise typer.Exit(code=1)
|
|
153
|
+
|
|
154
|
+
# 读取输入文件内容
|
|
155
|
+
original_content: str | None = None
|
|
156
|
+
stem: str = "topic"
|
|
157
|
+
|
|
158
|
+
if input_files:
|
|
159
|
+
try:
|
|
160
|
+
original_content = read_files(input_files)
|
|
161
|
+
stem = input_files[0].stem
|
|
162
|
+
except (FileNotFoundError, ValueError) as e:
|
|
163
|
+
console.print(f"[red]读取文件失败:[/] {e}")
|
|
164
|
+
raise typer.Exit(code=1)
|
|
165
|
+
elif topic:
|
|
166
|
+
stem = make_stem_from_topic(topic)
|
|
167
|
+
|
|
168
|
+
# 显示信息面板
|
|
169
|
+
style_label = _get_style_label(effective_scene, effective_style)
|
|
170
|
+
info_lines = [
|
|
171
|
+
f"[bold blue]场景:[/] {effective_scene} [bold blue]风格:[/] {style_label}",
|
|
172
|
+
f"[bold blue]数量:[/] {effective_count}",
|
|
173
|
+
]
|
|
174
|
+
if topic:
|
|
175
|
+
info_lines.insert(0, f"[bold blue]主题:[/] {topic}")
|
|
176
|
+
if input_files:
|
|
177
|
+
files_str = ", ".join(f.name for f in input_files)
|
|
178
|
+
info_lines.insert(0, f"[bold blue]输入文件:[/] {files_str}")
|
|
179
|
+
|
|
180
|
+
console.print(Panel("\n".join(info_lines), title="replyme", border_style="blue"))
|
|
181
|
+
|
|
182
|
+
# 生成消息
|
|
183
|
+
def on_generated(index: int, message: str) -> None:
|
|
184
|
+
_print_message(index, effective_count, message)
|
|
185
|
+
if effective_interval > 0 and index < effective_count - 1:
|
|
186
|
+
console.print(f"\n[dim]等待 {effective_interval} 秒后生成下一条...[/dim]")
|
|
187
|
+
time.sleep(effective_interval)
|
|
188
|
+
|
|
189
|
+
messages = generate_series(
|
|
190
|
+
topic=topic,
|
|
191
|
+
count=effective_count,
|
|
192
|
+
original_content=original_content,
|
|
193
|
+
scene=effective_scene,
|
|
194
|
+
style=effective_style,
|
|
195
|
+
model=effective_model,
|
|
196
|
+
on_generated=on_generated,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
# 保存日志
|
|
200
|
+
log_path = save_follow_up_log(
|
|
201
|
+
stem=stem,
|
|
202
|
+
scene=effective_scene,
|
|
203
|
+
style=effective_style,
|
|
204
|
+
topic=topic,
|
|
205
|
+
original_content=original_content,
|
|
206
|
+
generated_messages=messages,
|
|
207
|
+
output_dir=output_dir,
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# 打印汇总
|
|
211
|
+
success_count = sum(1 for m in messages if m)
|
|
212
|
+
console.print(
|
|
213
|
+
f"\n[bold green]完成![/bold green] "
|
|
214
|
+
f"成功生成 {success_count}/{effective_count} 条消息。"
|
|
215
|
+
)
|
|
216
|
+
console.print(f"[dim]日志已保存: {log_path}[/dim]")
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ── 辅助函数 ────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
def _default_style(scene: str) -> str:
|
|
222
|
+
if scene == "email":
|
|
223
|
+
return "formal"
|
|
224
|
+
return "polite"
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _get_style_label(scene: str, style: str) -> str:
|
|
228
|
+
styles_map = EMAIL_STYLES if scene == "email" else IM_STYLES
|
|
229
|
+
return styles_map.get(style, style)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _print_message(index: int, total: int, message: str) -> None:
|
|
233
|
+
num = Text(f"#{index + 1}/{total}", style="bold cyan")
|
|
234
|
+
panel = Panel(message, title=num, border_style="green", padding=(0, 1))
|
|
235
|
+
console.print(panel)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def _show_config(cfg: ReplymeConfig) -> None:
|
|
239
|
+
table = Table(title="当前配置", show_header=False)
|
|
240
|
+
table.add_column("键", style="bold")
|
|
241
|
+
table.add_column("值")
|
|
242
|
+
table.add_row("场景", cfg.scene)
|
|
243
|
+
table.add_row("风格", cfg.style)
|
|
244
|
+
table.add_row("模型", cfg.model or "(llmdog 默认)")
|
|
245
|
+
table.add_row("数量", str(cfg.count))
|
|
246
|
+
table.add_row("间隔", f"{cfg.interval} 秒")
|
|
247
|
+
console.print(table)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
if __name__ == "__main__":
|
|
251
|
+
app()
|
replyme/config.py
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
"""replyme 本地配置管理:init 命令、配置文件读写、自定义提示词。"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from dataclasses import dataclass, field, asdict
|
|
8
|
+
|
|
9
|
+
# 默认配置目录名
|
|
10
|
+
_CONFIG_DIR = "replyme"
|
|
11
|
+
_CONFIG_FILE = "config.json"
|
|
12
|
+
_PROMPTS_FILE = "prompts.json"
|
|
13
|
+
|
|
14
|
+
# ── 场景与风格 ──────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
# 邮件风格
|
|
17
|
+
EMAIL_STYLES = {
|
|
18
|
+
"formal": "正式商务",
|
|
19
|
+
"friendly": "友好亲切",
|
|
20
|
+
"persistent": "坚持不懈",
|
|
21
|
+
"angry": "愤怒警告",
|
|
22
|
+
"poor": "可怜求助",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# IM 风格
|
|
26
|
+
IM_STYLES = {
|
|
27
|
+
"casual": "轻松随意",
|
|
28
|
+
"polite": "礼貌得体",
|
|
29
|
+
"persistent": "坚持不懈",
|
|
30
|
+
"angry": "愤怒警告",
|
|
31
|
+
"poor": "可怜求助",
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# ── 内置提示词模板 ──────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
_BUILTIN_PROMPTS: dict = {
|
|
37
|
+
"email": {
|
|
38
|
+
"formal": (
|
|
39
|
+
"你是一个跟进邮件撰写助手。\n\n"
|
|
40
|
+
"场景:我之前给对方发过邮件,但对方一直没有回复。我需要继续发跟进邮件,"
|
|
41
|
+
"礼貌地推动对方回复。\n\n"
|
|
42
|
+
"你的工作流程:\n"
|
|
43
|
+
"1. 先理解我之前发了什么邮件、核心诉求是什么\n"
|
|
44
|
+
"2. 基于这个理解,写一封新的跟进邮件,换一个角度或切入点来推动回复\n\n"
|
|
45
|
+
"绝对规则(必须遵守):\n"
|
|
46
|
+
"- 只输出一封邮件的正文纯文本,不要输出其他任何内容\n"
|
|
47
|
+
"- 绝对不要输出多封邮件供选择,只输出一封\n"
|
|
48
|
+
"- 不要加任何说明、解释、注释、编号、选项、分析过程\n"
|
|
49
|
+
"- 不要使用markdown格式(不要加粗、列表、代码块等)\n"
|
|
50
|
+
"- 语气正式、专业、礼貌,体现商务礼仪\n"
|
|
51
|
+
"- 字数控制在80-200字\n"
|
|
52
|
+
"- 新邮件必须与之前发过的所有邮件(包括原始邮件和跟进邮件)明显不同\n"
|
|
53
|
+
"- 每次跟进换一个角度:可以补充信息、提供帮助、说明紧迫性、提出具体问题等"
|
|
54
|
+
),
|
|
55
|
+
"friendly": (
|
|
56
|
+
"你是一个跟进邮件撰写助手。\n\n"
|
|
57
|
+
"场景:我之前给对方发过邮件,但对方一直没有回复。我需要继续发跟进邮件,"
|
|
58
|
+
"友好地推动对方回复。\n\n"
|
|
59
|
+
"你的工作流程:\n"
|
|
60
|
+
"1. 先理解我之前发了什么邮件、核心诉求是什么\n"
|
|
61
|
+
"2. 基于这个理解,写一封新的跟进邮件,换一个角度或切入点来推动回复\n\n"
|
|
62
|
+
"绝对规则(必须遵守):\n"
|
|
63
|
+
"- 只输出一封邮件的正文纯文本,不要输出其他任何内容\n"
|
|
64
|
+
"- 绝对不要输出多封邮件供选择,只输出一封\n"
|
|
65
|
+
"- 不要加任何说明、解释、注释、编号、选项、分析过程\n"
|
|
66
|
+
"- 不要使用markdown格式(不要加粗、列表、代码块等)\n"
|
|
67
|
+
"- 语气友好、亲切、自然,像同事之间沟通\n"
|
|
68
|
+
"- 字数控制在80-200字\n"
|
|
69
|
+
"- 新邮件必须与之前发过的所有邮件明显不同\n"
|
|
70
|
+
"- 每次跟进换一个角度:可以表达关心、分享相关进展、询问是否需要帮助等"
|
|
71
|
+
),
|
|
72
|
+
"persistent": (
|
|
73
|
+
"你是一个跟进邮件撰写助手。\n\n"
|
|
74
|
+
"场景:我之前给对方发过邮件,但对方一直没有回复。我需要继续发跟进邮件,"
|
|
75
|
+
"礼貌但坚定地推动对方回复。\n\n"
|
|
76
|
+
"你的工作流程:\n"
|
|
77
|
+
"1. 先理解我之前发了什么邮件、核心诉求是什么\n"
|
|
78
|
+
"2. 基于这个理解,写一封新的跟进邮件,换一个角度或切入点来推动回复\n\n"
|
|
79
|
+
"绝对规则(必须遵守):\n"
|
|
80
|
+
"- 只输出一封邮件的正文纯文本,不要输出其他任何内容\n"
|
|
81
|
+
"- 绝对不要输出多封邮件供选择,只输出一封\n"
|
|
82
|
+
"- 不要加任何说明、解释、注释、编号、选项、分析过程\n"
|
|
83
|
+
"- 不要使用markdown格式(不要加粗、列表、代码块等)\n"
|
|
84
|
+
"- 语气礼貌但坚定,让对方感受到这件事确实需要推进\n"
|
|
85
|
+
"- 字数控制在80-200字\n"
|
|
86
|
+
"- 新邮件必须与之前发过的所有邮件明显不同\n"
|
|
87
|
+
"- 每次跟进换一个角度:可以说明影响和风险、设定时间节点、请求具体行动等"
|
|
88
|
+
),
|
|
89
|
+
"angry": (
|
|
90
|
+
"你是一个跟进邮件撰写助手。\n\n"
|
|
91
|
+
"场景:我之前给对方发过邮件,但对方一直没有回复。我需要继续发跟进邮件,"
|
|
92
|
+
"用愤怒但仍保持基本礼貌的语气表达不满,推动对方回复。\n\n"
|
|
93
|
+
"你的工作流程:\n"
|
|
94
|
+
"1. 先理解我之前发了什么邮件、核心诉求是什么\n"
|
|
95
|
+
"2. 基于这个理解,写一封新的跟进邮件,换一个角度或切入点来推动回复\n\n"
|
|
96
|
+
"绝对规则(必须遵守):\n"
|
|
97
|
+
"- 只输出一封邮件的正文纯文本,不要输出其他任何内容\n"
|
|
98
|
+
"- 绝对不要输出多封邮件供选择,只输出一封\n"
|
|
99
|
+
"- 不要加任何说明、解释、注释、编号、选项、分析过程\n"
|
|
100
|
+
"- 不要使用markdown格式(不要加粗、列表、代码块等)\n"
|
|
101
|
+
"- 语气表达强烈不满但仍保持基本商务礼仪,不能人身攻击或使用粗俗语言\n"
|
|
102
|
+
"- 字数控制在80-200字\n"
|
|
103
|
+
"- 新邮件必须与之前发过的所有邮件明显不同\n"
|
|
104
|
+
"- 每次跟进换一个角度:可以质问拖延原因、说明已严重影响你的计划、暗示后果等"
|
|
105
|
+
),
|
|
106
|
+
"poor": (
|
|
107
|
+
"你是一个跟进邮件撰写助手。\n\n"
|
|
108
|
+
"场景:我之前给对方发过邮件,但对方一直没有回复。我需要继续发跟进邮件,"
|
|
109
|
+
"用示弱、博取同情的语气推动对方回复。\n\n"
|
|
110
|
+
"你的工作流程:\n"
|
|
111
|
+
"1. 先理解我之前发了什么邮件、核心诉求是什么\n"
|
|
112
|
+
"2. 基于这个理解,写一封新的跟进邮件,换一个角度或切入点来推动回复\n\n"
|
|
113
|
+
"绝对规则(必须遵守):\n"
|
|
114
|
+
"- 只输出一封邮件的正文纯文本,不要输出其他任何内容\n"
|
|
115
|
+
"- 绝对不要输出多封邮件供选择,只输出一封\n"
|
|
116
|
+
"- 不要加任何说明、解释、注释、编号、选项、分析过程\n"
|
|
117
|
+
"- 不要使用markdown格式(不要加粗、列表、代码块等)\n"
|
|
118
|
+
"- 语气真诚示弱但不要卑微到失去尊严,不要道德绑架或过分诉苦\n"
|
|
119
|
+
"- 字数控制在80-200字\n"
|
|
120
|
+
"- 新邮件必须与之前发过的所有邮件明显不同\n"
|
|
121
|
+
"- 每次跟进换一个角度:可以说明自己的困境、表达无奈、请求帮助等"
|
|
122
|
+
),
|
|
123
|
+
},
|
|
124
|
+
"im": {
|
|
125
|
+
"casual": (
|
|
126
|
+
"你是一个跟进消息撰写助手。\n\n"
|
|
127
|
+
"场景:我之前给对方发过消息,但对方一直没有回复。我需要继续发跟进消息,"
|
|
128
|
+
"轻松地推动对方回复。\n\n"
|
|
129
|
+
"你的工作流程:\n"
|
|
130
|
+
"1. 先理解我之前发了什么消息、核心诉求是什么\n"
|
|
131
|
+
"2. 基于这个理解,写一条新的跟进消息,换一个角度或切入点来推动回复\n\n"
|
|
132
|
+
"绝对规则(必须遵守):\n"
|
|
133
|
+
"- 只输出一条消息的纯文本内容,不要输出其他任何内容\n"
|
|
134
|
+
"- 绝对不要输出多条消息供选择,只输出一条\n"
|
|
135
|
+
"- 不要加任何说明、解释、注释、编号、引号、分析过程\n"
|
|
136
|
+
"- 不要使用markdown格式\n"
|
|
137
|
+
"- 语气轻松随意,像朋友之间发微信一样自然\n"
|
|
138
|
+
"- 字数控制在30-80字\n"
|
|
139
|
+
"- 新消息必须与之前发过的所有消息明显不同\n"
|
|
140
|
+
"- 每次跟进换一个角度:可以换个说法、加点新信息、用幽默感等"
|
|
141
|
+
),
|
|
142
|
+
"polite": (
|
|
143
|
+
"你是一个跟进消息撰写助手。\n\n"
|
|
144
|
+
"场景:我之前给对方发过消息,但对方一直没有回复。我需要继续发跟进消息,"
|
|
145
|
+
"礼貌地推动对方回复。\n\n"
|
|
146
|
+
"你的工作流程:\n"
|
|
147
|
+
"1. 先理解我之前发了什么消息、核心诉求是什么\n"
|
|
148
|
+
"2. 基于这个理解,写一条新的跟进消息,换一个角度或切入点来推动回复\n\n"
|
|
149
|
+
"绝对规则(必须遵守):\n"
|
|
150
|
+
"- 只输出一条消息的纯文本内容,不要输出其他任何内容\n"
|
|
151
|
+
"- 绝对不要输出多条消息供选择,只输出一条\n"
|
|
152
|
+
"- 不要加任何说明、解释、注释、编号、引号、分析过程\n"
|
|
153
|
+
"- 不要使用markdown格式\n"
|
|
154
|
+
"- 语气礼貌温和,得体但不失亲切\n"
|
|
155
|
+
"- 字数控制在40-100字\n"
|
|
156
|
+
"- 新消息必须与之前发过的所有消息明显不同\n"
|
|
157
|
+
"- 每次跟进换一个角度:可以补充背景、提供选项、表达理解对方忙碌等"
|
|
158
|
+
),
|
|
159
|
+
"persistent": (
|
|
160
|
+
"你是一个跟进消息撰写助手。\n\n"
|
|
161
|
+
"场景:我之前给对方发过消息,但对方一直没有回复。我需要继续发跟进消息,"
|
|
162
|
+
"礼貌但明确地推动对方回复。\n\n"
|
|
163
|
+
"你的工作流程:\n"
|
|
164
|
+
"1. 先理解我之前发了什么消息、核心诉求是什么\n"
|
|
165
|
+
"2. 基于这个理解,写一条新的跟进消息,换一个角度或切入点来推动回复\n\n"
|
|
166
|
+
"绝对规则(必须遵守):\n"
|
|
167
|
+
"- 只输出一条消息的纯文本内容,不要输出其他任何内容\n"
|
|
168
|
+
"- 绝对不要输出多条消息供选择,只输出一条\n"
|
|
169
|
+
"- 不要加任何说明、解释、注释、编号、引号、分析过程\n"
|
|
170
|
+
"- 不要使用markdown格式\n"
|
|
171
|
+
"- 语气礼貌但坚定,让对方感受到你确实需要回复\n"
|
|
172
|
+
"- 字数控制在40-100字\n"
|
|
173
|
+
"- 新消息必须与之前发过的所有消息明显不同\n"
|
|
174
|
+
"- 每次跟进换一个角度:可以说明事情的影响、请求具体时间、直接提问等"
|
|
175
|
+
),
|
|
176
|
+
"angry": (
|
|
177
|
+
"你是一个跟进消息撰写助手。\n\n"
|
|
178
|
+
"场景:我之前给对方发过消息,但对方一直没有回复。我需要继续发跟进消息,"
|
|
179
|
+
"用愤怒但仍保持基本礼貌的语气表达不满,推动对方回复。\n\n"
|
|
180
|
+
"你的工作流程:\n"
|
|
181
|
+
"1. 先理解我之前发了什么消息、核心诉求是什么\n"
|
|
182
|
+
"2. 基于这个理解,写一条新的跟进消息,换一个角度或切入点来推动回复\n\n"
|
|
183
|
+
"绝对规则(必须遵守):\n"
|
|
184
|
+
"- 只输出一条消息的纯文本内容,不要输出其他任何内容\n"
|
|
185
|
+
"- 绝对不要输出多条消息供选择,只输出一条\n"
|
|
186
|
+
"- 不要加任何说明、解释、注释、编号、引号、分析过程\n"
|
|
187
|
+
"- 不要使用markdown格式\n"
|
|
188
|
+
"- 语气表达强烈不满但仍保持基本礼貌,不能人身攻击或使用粗俗语言\n"
|
|
189
|
+
"- 字数控制在40-100字\n"
|
|
190
|
+
"- 新消息必须与之前发过的所有消息明显不同\n"
|
|
191
|
+
"- 每次跟进换一个角度:可以质问为什么已读不回、说明对你的影响、暗示后果等"
|
|
192
|
+
),
|
|
193
|
+
"poor": (
|
|
194
|
+
"你是一个跟进消息撰写助手。\n\n"
|
|
195
|
+
"场景:我之前给对方发过消息,但对方一直没有回复。我需要继续发跟进消息,"
|
|
196
|
+
"用示弱、博取同情的语气推动对方回复。\n\n"
|
|
197
|
+
"你的工作流程:\n"
|
|
198
|
+
"1. 先理解我之前发了什么消息、核心诉求是什么\n"
|
|
199
|
+
"2. 基于这个理解,写一条新的跟进消息,换一个角度或切入点来推动回复\n\n"
|
|
200
|
+
"绝对规则(必须遵守):\n"
|
|
201
|
+
"- 只输出一条消息的纯文本内容,不要输出其他任何内容\n"
|
|
202
|
+
"- 绝对不要输出多条消息供选择,只输出一条\n"
|
|
203
|
+
"- 不要加任何说明、解释、注释、编号、引号、分析过程\n"
|
|
204
|
+
"- 不要使用markdown格式\n"
|
|
205
|
+
"- 语气真诚示弱但不要卑微到失去尊严,不要道德绑架或过分诉苦\n"
|
|
206
|
+
"- 字数控制在40-100字\n"
|
|
207
|
+
"- 新消息必须与之前发过的所有消息明显不同\n"
|
|
208
|
+
"- 每次跟进换一个角度:可以表达无奈、说自己的困境、求帮忙等"
|
|
209
|
+
),
|
|
210
|
+
},
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
@dataclass
|
|
215
|
+
class ReplymeConfig:
|
|
216
|
+
"""replyme 运行时配置。"""
|
|
217
|
+
|
|
218
|
+
scene: str = "im" # "email" 或 "im"
|
|
219
|
+
style: str = "polite" # 风格名称
|
|
220
|
+
model: str | None = None # LLM 模型
|
|
221
|
+
count: int = 5 # 生成消息数量
|
|
222
|
+
interval: int = 0 # 消息间隔秒数
|
|
223
|
+
|
|
224
|
+
def to_dict(self) -> dict:
|
|
225
|
+
return asdict(self)
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def from_dict(cls, d: dict) -> ReplymeConfig:
|
|
229
|
+
return cls(**{k: v for k, v in d.items() if k in cls.__dataclass_fields__})
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def get_config_dir() -> Path:
|
|
233
|
+
"""获取配置目录路径(当前目录下的 replyme)。"""
|
|
234
|
+
return Path.cwd() / _CONFIG_DIR
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def load_config() -> ReplymeConfig:
|
|
238
|
+
"""从本地配置文件加载配置,不存在则返回默认。"""
|
|
239
|
+
config_path = get_config_dir() / _CONFIG_FILE
|
|
240
|
+
if config_path.exists():
|
|
241
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
242
|
+
data = json.load(f)
|
|
243
|
+
return ReplymeConfig.from_dict(data)
|
|
244
|
+
return ReplymeConfig()
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def save_config(cfg: ReplymeConfig) -> Path:
|
|
248
|
+
"""保存配置到本地文件。"""
|
|
249
|
+
config_dir = get_config_dir()
|
|
250
|
+
config_dir.mkdir(exist_ok=True)
|
|
251
|
+
config_path = config_dir / _CONFIG_FILE
|
|
252
|
+
with open(config_path, "w", encoding="utf-8") as f:
|
|
253
|
+
json.dump(cfg.to_dict(), f, ensure_ascii=False, indent=2)
|
|
254
|
+
return config_path
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def load_prompts() -> dict:
|
|
258
|
+
"""加载提示词配置,本地文件优先,否则用内置默认。"""
|
|
259
|
+
prompts_path = get_config_dir() / _PROMPTS_FILE
|
|
260
|
+
if prompts_path.exists():
|
|
261
|
+
with open(prompts_path, "r", encoding="utf-8") as f:
|
|
262
|
+
return json.load(f)
|
|
263
|
+
return _BUILTIN_PROMPTS
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
def save_prompts(prompts: dict | None = None) -> Path:
|
|
267
|
+
"""保存提示词到本地文件。默认保存内置提示词。"""
|
|
268
|
+
config_dir = get_config_dir()
|
|
269
|
+
config_dir.mkdir(exist_ok=True)
|
|
270
|
+
prompts_path = config_dir / _PROMPTS_FILE
|
|
271
|
+
data = prompts if prompts is not None else _BUILTIN_PROMPTS
|
|
272
|
+
with open(prompts_path, "w", encoding="utf-8") as f:
|
|
273
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
274
|
+
return prompts_path
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
def get_system_prompt(scene: str, style: str) -> str:
|
|
278
|
+
"""获取指定场景和风格的系统提示词。"""
|
|
279
|
+
prompts = load_prompts()
|
|
280
|
+
scene_prompts = prompts.get(scene, {})
|
|
281
|
+
prompt = scene_prompts.get(style)
|
|
282
|
+
if prompt:
|
|
283
|
+
return prompt
|
|
284
|
+
# 回退到该场景的第一个风格
|
|
285
|
+
if scene_prompts:
|
|
286
|
+
return next(iter(scene_prompts.values()))
|
|
287
|
+
# 最终回退到 IM polite
|
|
288
|
+
return _BUILTIN_PROMPTS["im"]["polite"]
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def init_config() -> dict[str, Path]:
|
|
292
|
+
"""初始化本地配置目录和文件,返回创建的文件路径。"""
|
|
293
|
+
config_dir = get_config_dir()
|
|
294
|
+
config_dir.mkdir(exist_ok=True)
|
|
295
|
+
|
|
296
|
+
config_path = save_config(ReplymeConfig())
|
|
297
|
+
prompts_path = save_prompts()
|
|
298
|
+
|
|
299
|
+
return {"config": config_path, "prompts": prompts_path}
|