mailcode 0.1.0__tar.gz → 0.1.3__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.
Files changed (33) hide show
  1. {mailcode-0.1.0 → mailcode-0.1.3}/PKG-INFO +1 -1
  2. mailcode-0.1.3/README.md +252 -0
  3. mailcode-0.1.3/mailcode/__init__.py +1 -0
  4. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/cli.py +130 -6
  5. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/config.py +26 -0
  6. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/relay/conversation_handler.py +13 -34
  7. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/relay/email_listener.py +294 -61
  8. mailcode-0.1.3/mailcode/relay/scheduler.py +816 -0
  9. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/relay/stateless_handler.py +9 -4
  10. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/resources/default.json +12 -0
  11. mailcode-0.1.3/mailcode/schedule_cli.py +471 -0
  12. mailcode-0.1.3/mailcode/server.py +70 -0
  13. mailcode-0.1.3/mailcode/utils/claude_runner.py +44 -0
  14. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode.egg-info/PKG-INFO +1 -1
  15. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode.egg-info/SOURCES.txt +3 -0
  16. {mailcode-0.1.0 → mailcode-0.1.3}/pyproject.toml +1 -1
  17. mailcode-0.1.0/README.md +0 -151
  18. mailcode-0.1.0/mailcode/__init__.py +0 -1
  19. mailcode-0.1.0/mailcode/server.py +0 -40
  20. {mailcode-0.1.0 → mailcode-0.1.3}/LICENSE +0 -0
  21. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/channels/__init__.py +0 -0
  22. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/channels/email_channel.py +0 -0
  23. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/health.py +0 -0
  24. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/provider_presets.py +0 -0
  25. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/relay/__init__.py +0 -0
  26. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/relay/security.py +0 -0
  27. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/session_cli.py +0 -0
  28. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/utils/__init__.py +0 -0
  29. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode/utils/logging.py +0 -0
  30. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode.egg-info/dependency_links.txt +0 -0
  31. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode.egg-info/entry_points.txt +0 -0
  32. {mailcode-0.1.0 → mailcode-0.1.3}/mailcode.egg-info/top_level.txt +0 -0
  33. {mailcode-0.1.0 → mailcode-0.1.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mailcode
3
- Version: 0.1.0
3
+ Version: 0.1.3
4
4
  Summary: Email ↔ AI Agent bidirectional remote command bridge
5
5
  License: MIT
6
6
  Requires-Python: >=3.9
@@ -0,0 +1,252 @@
1
+ # MailCode
2
+
3
+ Python 邮件连接器,通过邮件远程操控 Claude Code。
4
+
5
+ ```
6
+ 收件箱 ──> IMAP 监听器 ──> claude -p 子进程 ──> SMTP 邮件通知
7
+ ```
8
+
9
+ ## 设计理念
10
+
11
+ MailCode 的核心理念是**轻量化的人与 Coding Agent 直连**。
12
+
13
+ 市面上的 AI 工具链往往依赖飞书、钉钉等重型协作平台——建机器人、配 Webhook、在对话框里和 Agent 来回聊天。MailCode 反其道而行:直接用邮件,因为你本来就有一个邮箱。
14
+
15
+ **人与 Agent 直连,而不是和机器人对话。** 回复邮件就是下达指令,收件箱就是控制台,不需要打开任何第三方应用。
16
+
17
+ **轻量异步。** 不需要常驻复杂服务,不需要数据库,不需要消息队列。一个 Python 脚本 + 邮件协议,跑在任何能联网的机器上。Agent 在后台慢慢跑,你做别的事,完事了邮件通知你。
18
+
19
+ MailCode 不做大而全的平台,只做一件事:**让你用最习惯的方式(邮件)和 Agent 对话。**
20
+
21
+ ## 邮箱账户架构
22
+
23
+ MailCode 需要 **两个邮箱账户**——一个当 Bot,一个当用户:
24
+
25
+ - **Bot 邮箱**(例:`mailcode_bot@xxx.com`)—— MailCode 监听它的收件箱,Claude 处理完任务后通过它把结果发回给你
26
+ - **用户邮箱**(你的私人邮箱,例:`your@qq.com`)—— 你从这个邮箱向 Bot 邮箱发邮件下达指令
27
+
28
+ 工作流:
29
+
30
+ ```
31
+ [用户邮箱] ──发指令邮件──▶ [Bot 邮箱收件箱]
32
+ your@qq.com mailcode_bot@xxx.com
33
+
34
+
35
+ IMAP 监听
36
+
37
+
38
+ Claude 处理
39
+
40
+
41
+ [用户邮箱] ◀──回复邮件── [Bot 邮箱发件箱]
42
+ ```
43
+
44
+ > **为什么是 Bot 邮箱而不是你自己的主邮箱?** MailCode 要登录一个邮箱才能收信和发信,所以需要一个专用 Bot 邮箱;它和你日常使用的邮箱是分开的,配置 `allowed_senders` 限制只有你自己的私人邮箱能给它发指令。
45
+
46
+ ## 安装
47
+
48
+ ### 系统依赖
49
+
50
+ - **python3**(≥3.9)
51
+ - **Claude Code**(`claude` 命令需在 `PATH` 中)
52
+
53
+ Python 零第三方依赖,全部使用标准库(`imaplib`、`smtplib`、`email`、`subprocess`、`json`、`secrets` 等)。
54
+
55
+ ### pip 安装
56
+
57
+ ```bash
58
+ pip install mailcode
59
+ ```
60
+
61
+ ### 源码安装
62
+
63
+ ```bash
64
+ git clone <repo-url> && cd MailCode
65
+ bash install.sh
66
+ ```
67
+
68
+ `install.sh` 自动完成:安装 mailcode 包、初始化配置、创建 `~/.mailcode` 软链接、自动添加 PATH。
69
+
70
+ 从本地 wheel 安装:`bash install.sh --local dist/mailcode-*.whl`
71
+
72
+ ## 配置
73
+
74
+ 编辑 `~/.config/mailcode/config.json`,必填字段。**两个邮箱要分清楚**——`mailcode_bot.email` 是 Bot 邮箱,`security.allowed_senders` 是允许给它发指令的邮箱(通常是你自己的私人邮箱):
75
+
76
+ ```jsonc
77
+ {
78
+ "mailcode_bot": {
79
+ "email": "mailcode_bot@xxx.com", // ← Bot 邮箱:MailCode 登录此邮箱收信/回信
80
+ "password": "Bot 邮箱授权码", // ← Bot 邮箱的授权码,不是登录密码
81
+ "check_interval": 60 // ← 轮询间隔(秒); 163/126 推荐 60-120
82
+ },
83
+ "security": {
84
+ "allowed_senders": ["your@qq.com"] // ← 允许发指令的邮箱(你的私人邮箱)
85
+ }
86
+ }
87
+ ```
88
+
89
+ SMTP 和 IMAP 配置由系统根据 Bot 邮箱的域名自动识别。支持:QQ 邮箱、163/126 邮箱、Gmail、Outlook/Hotmail。
90
+
91
+ 如需手动覆盖 SMTP/IMAP(如自建邮箱),可添加 `smtp` / `imap` 段,手动设置的值会覆盖自动识别结果。
92
+
93
+ > 授权码获取:QQ 邮箱 → 设置 → 账户 → POP3/IMAP → 生成授权码;Gmail → Google 账户 → 安全性 → 应用密码。
94
+
95
+ ## 使用
96
+
97
+ ### CLI 概览
98
+
99
+ | 子命令 | 用途 |
100
+ |--------|------|
101
+ | `mailcode serve` | 启动 IMAP 监听中继(含定时任务调度器)|
102
+ | `mailcode schedule <动作>` | 定时任务管理(`list` / `show` / `add` / `enable` / `disable` / `delete` / `run-now` / `validate`)|
103
+ | `mailcode config <动作>` | 配置管理(`show` / `init` / `init-test` / `path` / `validate`)|
104
+ | `mailcode health` | 邮件连通性检查(SMTP/IMAP)|
105
+ | `mailcode session <动作>` | 会话管理(`list` / `show` / `delete` / `cleanup`)|
106
+ | `mailcode --version` | 显示版本号 |
107
+
108
+ ### 启动中继
109
+
110
+ ```bash
111
+ # 前台运行(默认 IMAP IDLE 长连接, 单连接撑全场, 实时收信)
112
+ mailcode serve
113
+
114
+ # 干跑模式(仅打印邮件, 不调用 claude)
115
+ mailcode serve --dry-run
116
+
117
+ # 强制走轮询(不用 IDLE; 部分老旧邮箱要求)
118
+ mailcode serve --no-idle
119
+
120
+ # 单次轮询后退出
121
+ mailcode serve --once
122
+ ```
123
+
124
+ **IMAP IDLE 支持按邮箱而异**——MailCode 启动时检测 `IMAP CAPABILITY`, 没有 IDLE 就自动回退到轮询:
125
+
126
+ | 邮箱 | IDLE | 行为 | 推荐 `check_interval` |
127
+ |------|------|------|----------------------|
128
+ | QQ 邮箱 (`imap.qq.com`) | ✅ | 实时推送, 秒级响应 | 60s(轮询时)|
129
+ | 163/126 邮箱 (`imap.163.com` / `imap.126.com`) | ❌ | 自动回退到轮询, warning 日志告知 | **60-120s**(频率过高会被反滥用限速, 严重时封 IP)|
130
+ | Gmail / Outlook | ✅ | 实时推送 | 60s(轮询时)|
131
+
132
+ 网易系邮箱**不支持 IDLE 扩展**, 频繁 IMAP 登录会触发反滥用。Bot 邮箱若用 163/126, 务必把 `mailcode_bot.check_interval` 调到 60-120 秒, 否则几小时内可能被临时封禁。
133
+
134
+ 查看日志:
135
+
136
+ ```bash
137
+ tail -f ~/.config/mailcode/relay.log
138
+ ```
139
+
140
+ ### 配置管理
141
+
142
+ ```bash
143
+ mailcode config show # 查看当前配置(密码脱敏)
144
+ mailcode config path # 显示配置文件路径
145
+ mailcode config init # 初始化配置(已存在则跳过)
146
+ mailcode config init --force # 强制重新生成
147
+ mailcode config validate # 校验配置完整性
148
+ ```
149
+
150
+ ### 会话管理
151
+
152
+ MailCode 默认按邮件主题维护多轮对话; 如需单次回复模式请设 `session.enabled = false`。
153
+
154
+ ```bash
155
+ mailcode session list # 列出所有 session
156
+ mailcode session show <session_id> # 查看单个 session 详情
157
+ mailcode session delete <session_id> # 删除 session
158
+ mailcode session cleanup # 按 TTL 清理过期 session
159
+ mailcode session cleanup --dry-run # 仅预览,不实际删除
160
+ ```
161
+
162
+ ### 工作目录 (cwd 指令)
163
+
164
+ 在邮件正文**第一行**写 `cwd: <path>`,Claude 子进程会在该目录启动——适合「让 Claude 操作指定项目」。Session 模式下 cwd **粘性**:同一 session 内的后续邮件会沿用该目录,直到新邮件重新指定。
165
+
166
+ ```
167
+ cwd: ~/Projects/my-app
168
+ 帮我看看 src/auth.py 里那段 JWT 校验逻辑
169
+ ```
170
+
171
+ **路径解析规则**:
172
+
173
+ - `~` / `~/foo` 走用户目录展开
174
+ - 相对路径(`./foo`、`foo`)以 `Path.cwd()` 为基准
175
+ - 路径必须存在且是目录(`is_dir()` 校验),否则忽略并回退默认(`$HOME`)
176
+ - 写法不区分大小写,`Cwd:` / `CWD:` 等价
177
+
178
+ **两种模式差异**:
179
+
180
+ - **Session 模式**(`session.enabled = true` 默认):cwd 粘性,整个 session 沿用;`mailcode session show <id>` 可查当前 cwd
181
+ - **单次回复模式**(`session.enabled = false`):cwd 不粘性,每封邮件独立解析
182
+
183
+ cwd 行会在调用 Claude 前从 body 中剥离,不会污染 prompt。
184
+
185
+ ### 健康检查
186
+
187
+ ```bash
188
+ mailcode health # 检查 SMTP/IMAP 配置与连通性
189
+ ```
190
+
191
+ 检查项:SMTP 连接 / 登录 / 发信、IMAP 连接 / 登录 / 收件箱。
192
+
193
+ ### 定时任务
194
+
195
+ MailCode 内置轻量定时任务引擎,无需外部 cron 或 systemd timer——直接在 `mailcode serve` 内跑,配置持久化到 `~/.config/mailcode/schedules.json`。
196
+
197
+ 支持四种调度类型:
198
+
199
+ | 类型 | 参数 | 示例 |
200
+ |------|------|------|
201
+ | `interval` | `--interval-seconds <秒>` | 每 3600 秒运行一次 |
202
+ | `daily` | `--time <HH:MM>` | 每天 09:00 运行 |
203
+ | `weekly` | `--time <HH:MM> --day-of-week <0-6>` | 每周一 09:00(0=周日) |
204
+ | `monthly` | `--time <HH:MM> --day-of-month <1-31>` | 每月 1 号 09:00 |
205
+
206
+ 定时任务会调用 `claude -p <prompt>` 执行指定指令,并将 Claude 的响应邮件发送给指定收件人。所有调度基于本地时间,错过触发窗口的任务自动跳过(不会追赶补跑)。
207
+
208
+ ```bash
209
+ # 创建定时任务
210
+ mailcode schedule add morning-digest --type daily --time 09:00 \
211
+ --prompt "总结 GitHub 通知, 列出今天待办" \
212
+ --to-email your@qq.com \
213
+ --subject-prefix "[晨间摘要]"
214
+
215
+ # 列出所有任务
216
+ mailcode schedule list
217
+
218
+ # 查看任务详情
219
+ mailcode schedule show morning-digest
220
+
221
+ # 立即执行一次(不污染调度统计)
222
+ mailcode schedule run-now morning-digest
223
+
224
+ # 启用 / 禁用
225
+ mailcode schedule enable morning-digest
226
+ mailcode schedule disable morning-digest
227
+
228
+ # 删除
229
+ mailcode schedule delete morning-digest
230
+
231
+ # 校验所有任务配置
232
+ mailcode schedule validate
233
+ ```
234
+
235
+ **配置**(`~/.config/mailcode/config.json`,可选):
236
+
237
+ ```jsonc
238
+ {
239
+ "schedule": {
240
+ "enabled": true, // 全局开关
241
+ "tick_seconds": 30 // 调度器轮询间隔
242
+ }
243
+ }
244
+ ```
245
+
246
+ **特点**:
247
+
248
+ - **热加载**——通过 CLI 增删改任务后,运行中的 `serve` 进程立即生效,无需重启
249
+ - **并发防护**——同一任务的上次执行未完成时,不会重复触发
250
+ - **错误通知**——Claude 调用或邮件发送失败时,自动通知收件人
251
+ - **独立运行**——`mailcode schedule run-now` 不依赖 `serve` 进程,可单独使用
252
+ - **干跑兼容**——`mailcode serve --dry-run` 下标记执行结果但不真实调用
@@ -0,0 +1 @@
1
+ __version__ = "0.1.3"
@@ -77,6 +77,68 @@ def _build_session_handler():
77
77
  return ConversationHandler(email_channel=channel)
78
78
 
79
79
 
80
+ def _build_schedule_store():
81
+ """构造 ScheduleStore 实例(默认路径 ~/.config/mailcode/schedules.json)。"""
82
+ from pathlib import Path
83
+
84
+ schedules_path = Path.home() / ".config" / "mailcode" / "schedules.json"
85
+ from mailcode.relay.scheduler import ScheduleStore
86
+ return ScheduleStore(schedules_path)
87
+
88
+
89
+ def cmd_schedule(args):
90
+ """schedule 子命令: list/show/add/enable/disable/delete/run-now/validate。"""
91
+ from mailcode.schedule_cli import (
92
+ cmd_schedule_list, cmd_schedule_show, cmd_schedule_add,
93
+ cmd_schedule_enable, cmd_schedule_disable, cmd_schedule_delete,
94
+ cmd_schedule_run_now, cmd_schedule_validate,
95
+ )
96
+ sub = getattr(args, "schedule_command", None)
97
+ if sub is None:
98
+ print("用法: mailcode schedule <list|show|add|enable|disable|delete|run-now|validate>", file=sys.stderr)
99
+ sys.exit(1)
100
+
101
+ store = _build_schedule_store()
102
+
103
+ if sub == "list":
104
+ cmd_schedule_list(store)
105
+ elif sub == "show":
106
+ cmd_schedule_show(store, args.name)
107
+ elif sub == "add":
108
+ # 把 CLI 的 "mon" 字符串转为 int(0),与 parse_schedule 的整数 api 一致
109
+ _DOW_MAP = {"mon": 0, "tue": 1, "wed": 2, "thu": 3, "fri": 4, "sat": 5, "sun": 6}
110
+ cmd_schedule_add(store, args.name,
111
+ schedule_type=args.type,
112
+ interval_seconds=args.interval_seconds,
113
+ time=args.time,
114
+ day_of_week=_DOW_MAP.get(args.day_of_week) if args.day_of_week else None,
115
+ day_of_month=args.day_of_month,
116
+ prompt=args.prompt,
117
+ to_email=args.to_email,
118
+ cwd=args.cwd,
119
+ subject_prefix=args.subject_prefix,
120
+ interactive=False,
121
+ )
122
+ elif sub == "enable":
123
+ cmd_schedule_enable(store, args.name)
124
+ elif sub == "disable":
125
+ cmd_schedule_disable(store, args.name)
126
+ elif sub == "delete":
127
+ cmd_schedule_delete(store, args.name, assume_yes=args.yes)
128
+ elif sub == "run-now":
129
+ from mailcode.utils.claude_runner import call_claude
130
+ from mailcode.channels.email_channel import EmailChannel
131
+ cmd_schedule_run_now(store, args.name,
132
+ email_channel=EmailChannel(),
133
+ call_claude_fn=call_claude,
134
+ )
135
+ elif sub == "validate":
136
+ cmd_schedule_validate(store)
137
+ else:
138
+ print("用法: mailcode schedule <list|show|add|enable|disable|delete|run-now|validate>", file=sys.stderr)
139
+ sys.exit(1)
140
+
141
+
80
142
  def cmd_config(args):
81
143
  import json
82
144
  from mailcode.config import load_config, _ensure_user_config, get_config_path
@@ -228,7 +290,7 @@ def build_parser():
228
290
  "工作流: 发件人给机器人邮箱发邮件 → MailCode 在 IMAP 拉取 → 注入到本地 Agent\n"
229
291
  " → Agent 回复内容回写到同一主题 → MailCode 通过 SMTP 把回复转发给发件人。\n"
230
292
  "\n"
231
- "本 CLI 主要用于: 配置管理、连通性检查、启动中继、以及维护 Agent 对话 session。"
293
+ "本 CLI 主要用于: 配置管理、连通性检查、启动中继(含定时任务调度器)、维护 Agent 对话 session。"
232
294
  ),
233
295
  epilog=(
234
296
  "典型使用流程:\n"
@@ -236,8 +298,9 @@ def build_parser():
236
298
  " 2) 编辑授权码 mailcode config path && $EDITOR .../config.json\n"
237
299
  " 3) 校验配置 mailcode config validate\n"
238
300
  " 4) 自检连通性 mailcode health\n"
239
- " 5) 启动中继 mailcode serve --idle # 长期后台运行\n"
301
+ " 5) 启动中继 mailcode serve # 长期后台运行 (默认 IDLE 长连接, 含定时任务调度器)\n"
240
302
  " 6) 维护会话 mailcode session list|show|delete|cleanup\n"
303
+ " 7) 定时任务 mailcode schedule add/list|enable|disable|run-now\n"
241
304
  "\n"
242
305
  "调试提示:\n"
243
306
  " • 想看收到什么邮件但不想执行: mailcode serve --once --dry-run\n"
@@ -254,18 +317,21 @@ def build_parser():
254
317
  # ── serve ──
255
318
  p_serve = subparsers.add_parser(
256
319
  "serve",
257
- help="启动 IMAP 监听中继 (前台常驻)",
320
+ help="启动 IMAP 监听中继 (前台常驻, 含定时任务调度器)",
258
321
  description=(
259
322
  "启动 IMAP 监听中继: 拉取 bot 邮箱里的未读邮件, 注入本地 AI Agent, 把回复通过 SMTP 转发回发件人。\n"
323
+ "同时运行定时任务调度器: 按 schedules.json 中的配置在后台周期性执行 claude -p 并邮件通知结果。\n"
324
+ "默认使用 IMAP IDLE 长连接 (实时推送, 适用于 QQ / Gmail / Outlook)。\n"
325
+ "126/163 邮箱不支持 IDLE, 自动回退到轮询, 需将 check_interval 调到 60-120 秒避免反滥用。\n"
260
326
  "Ctrl-C 退出, 日志写入 ~/.config/mailcode/relay.log。"
261
327
  ),
262
328
  )
263
329
  p_serve.add_argument("--dry-run", action="store_true",
264
330
  help="干跑模式: 只打印邮件内容, 不注入 Agent, 不发送回复(用于排查邮件解析)")
265
331
  p_serve.add_argument("--once", action="store_true",
266
- help="处理完一轮未读后退出 (用于脚本/调试, 默认持续轮询)")
267
- p_serve.add_argument("--idle", action="store_true",
268
- help="使用 IMAP IDLE 长连接, 实时接收新邮件推送 (推荐生产环境使用)")
332
+ help="处理完一轮未读后退出 (调度器不启动, 用于脚本/调试, 默认持续监听)")
333
+ p_serve.add_argument("--no-idle", action="store_true",
334
+ help="禁用 IMAP IDLE 长连接, 改用固定间隔轮询 (默认启用 IDLE)")
269
335
  p_serve.add_argument("--session", "-S", action="store_true",
270
336
  help="按邮件主题维护多轮对话 session (覆盖 config 中 session.enabled)")
271
337
 
@@ -332,6 +398,62 @@ def build_parser():
332
398
  p_session_cleanup.add_argument("--dry-run", action="store_true",
333
399
  help="只列出将被清理的 session, 不实际删除")
334
400
 
401
+ # ── schedule ──
402
+ p_schedule = subparsers.add_parser(
403
+ "schedule",
404
+ help="定时任务管理 (list|show|add|enable|disable|delete|run-now|validate)",
405
+ description=(
406
+ "管理 ~/.config/mailcode/schedules.json 中的定时任务 (无需外部 cron)。\n"
407
+ "支持 interval(固定间隔)/ daily(每天定点)/ weekly(每周某天)/ monthly(每月某天)四种类型。\n"
408
+ "运行 mailcode serve 时调度器自动在后台运行, 到期触发 claude -p <prompt> 并邮件通知。\n"
409
+ "missed_run 自动跳过不追赶; 任务配置热加载, 增删改立即生效无需重启 serve。"
410
+ ),
411
+ )
412
+ p_schedule_sub = p_schedule.add_subparsers(
413
+ dest="schedule_command", title="schedule 动作", metavar="<动作>"
414
+ )
415
+
416
+ p_schedule_sub.add_parser("list", help="列出全部定时任务 (名称/类型/调度/状态/下次运行时间)")
417
+
418
+ p_schedule_sub.add_parser("validate", help="校验 schedules.json 完整性 (名称唯一/调度合法/邮箱有效/prompt 非空, 只读不改)")
419
+
420
+ p_schedule_show = p_schedule_sub.add_parser("show", help="查看单个定时任务详情")
421
+ p_schedule_show.add_argument("name", help="任务名称")
422
+
423
+ p_schedule_add = p_schedule_sub.add_parser("add", help="添加新定时任务")
424
+ p_schedule_add.add_argument("name", help="任务名称 (唯一)")
425
+ p_schedule_add.add_argument("--type", choices=["interval", "daily", "weekly", "monthly"],
426
+ required=True)
427
+ p_schedule_add.add_argument("--interval-seconds", type=int,
428
+ help="type=interval 时必填")
429
+ p_schedule_add.add_argument("--time", help="type=daily/weekly/monthly 时必填, HH:MM 格式")
430
+ p_schedule_add.add_argument("--day-of-week",
431
+ choices=["mon", "tue", "wed", "thu", "fri", "sat", "sun"],
432
+ help="type=weekly 时必填")
433
+ p_schedule_add.add_argument("--day-of-month", type=int, choices=range(1, 32),
434
+ metavar="[1-31]", help="type=monthly 时必填")
435
+ p_schedule_add.add_argument("--prompt", required=True, help="Claude prompt 文本")
436
+ p_schedule_add.add_argument("--to-email", required=True, help="结果邮件的收件人")
437
+ p_schedule_add.add_argument("--cwd", help="Claude 工作目录 (可选)")
438
+ p_schedule_add.add_argument("--subject-prefix", help="邮件主题前缀 (可选)")
439
+
440
+ p_schedule_enable = p_schedule_sub.add_parser("enable", help="启用一个定时任务")
441
+ p_schedule_enable.add_argument("name", help="任务名称")
442
+
443
+ p_schedule_disable = p_schedule_sub.add_parser("disable", help="禁用一个定时任务")
444
+ p_schedule_disable.add_argument("name", help="任务名称")
445
+
446
+ p_schedule_delete = p_schedule_sub.add_parser("delete", help="删除一个定时任务")
447
+ p_schedule_delete.add_argument("name", help="任务名称")
448
+ p_schedule_delete.add_argument("-y", "--yes", action="store_true",
449
+ help="跳过确认")
450
+
451
+ p_schedule_run = p_schedule_sub.add_parser(
452
+ "run-now",
453
+ help="立即同步执行指定任务 (不依赖 serve, 不污染 last_run_at / next_run_at)",
454
+ )
455
+ p_schedule_run.add_argument("name", help="任务名称")
456
+
335
457
  return parser
336
458
 
337
459
 
@@ -355,6 +477,8 @@ def main():
355
477
  cmd_health(args)
356
478
  elif args.command == "session":
357
479
  cmd_session(args)
480
+ elif args.command == "schedule":
481
+ cmd_schedule(args)
358
482
 
359
483
  if __name__ == "__main__":
360
484
  main()
@@ -197,6 +197,22 @@ def is_session_enabled() -> bool:
197
197
  return get_session_config().get("enabled", True)
198
198
 
199
199
 
200
+ SCHEDULE_DEFAULTS = {
201
+ "enabled": True,
202
+ "tick_seconds": 30,
203
+ "max_concurrent": 1,
204
+ "missed_run_policy": "skip", # v1 仅支持 skip, v2 加 run_immediately
205
+ }
206
+
207
+
208
+ def get_schedule_config() -> Dict[str, Any]:
209
+ config = load_config()
210
+ raw = config.get("schedule", {})
211
+ result = dict(SCHEDULE_DEFAULTS)
212
+ result.update(raw)
213
+ return result
214
+
215
+
200
216
  def validate_serve_config() -> list[str]:
201
217
  """校验 serve 启动所需配置项, 返回错误消息列表 (空列表 = 通过)。
202
218
 
@@ -241,6 +257,16 @@ def validate_serve_config() -> list[str]:
241
257
  allowed = security.get("allowed_senders", [])
242
258
  if not allowed:
243
259
  errors.append("security.allowed_senders 为空(至少应包含自己的邮箱)")
260
+
261
+ # schedule 段可选校验 (warn 而非阻塞)
262
+ schedule_cfg = config.get("schedule", {})
263
+ if schedule_cfg.get("enabled", True):
264
+ tick = schedule_cfg.get("tick_seconds", 30)
265
+ if not isinstance(tick, int) or tick <= 0:
266
+ errors.append(f"schedule.tick_seconds 应为正整数, 当前: {tick}")
267
+ max_c = schedule_cfg.get("max_concurrent", 1)
268
+ if not isinstance(max_c, int) or max_c < 1:
269
+ errors.append(f"schedule.max_concurrent 应为 >=1 整数, 当前: {max_c}")
244
270
  except Exception as e:
245
271
  logger.warning(f"配置校验过程中出错: {e}")
246
272
  errors.append(f"配置校验失败: {e}")
@@ -3,12 +3,13 @@
3
3
  import json
4
4
  import logging
5
5
  import re
6
- import subprocess
7
6
  import time
8
7
  import uuid
9
8
  from pathlib import Path
10
9
  from typing import Optional
11
10
 
11
+ from mailcode.utils import claude_runner as cr_module
12
+
12
13
  logger = logging.getLogger(__name__)
13
14
 
14
15
  # MailCode 主目录
@@ -24,9 +25,6 @@ _CWD_RE = re.compile(r"^cwd:\s*(.+?)\s*$", re.MULTILINE | re.IGNORECASE)
24
25
  # 默认 session TTL (天), 0 或负数 = 不清理
25
26
  _DEFAULT_TTL_DAYS = 90
26
27
 
27
- # claude -p 子进程超时
28
- _CLAUDE_TIMEOUT = 300
29
-
30
28
 
31
29
  # ------------------------------------------------------------------ #
32
30
  # 模块级纯函数 — 供 ConversationHandler / StatelessHandler 复用
@@ -64,34 +62,6 @@ def strip_cwd(body: str) -> str:
64
62
  return _CWD_RE.sub("", body).strip()
65
63
 
66
64
 
67
- def call_claude(prompt: str, cwd: str = "") -> Optional[str]:
68
- """调用 ``claude -p`` 子进程。失败返回 None。
69
-
70
- Args:
71
- prompt: 完整 prompt
72
- cwd: 工作目录 (默认 ``Path.home()``)
73
- """
74
- cwd = cwd or str(Path.home())
75
- try:
76
- result = subprocess.run(
77
- ["claude", "-p", prompt, "--dangerously-skip-permissions"],
78
- capture_output=True,
79
- text=True,
80
- timeout=_CLAUDE_TIMEOUT,
81
- cwd=cwd,
82
- )
83
- if result.returncode != 0:
84
- logger.error("claude -p 失败: %s", result.stderr[:500])
85
- return None
86
- return result.stdout.strip()
87
- except subprocess.TimeoutExpired:
88
- logger.error("claude -p 超时")
89
- return None
90
- except FileNotFoundError:
91
- logger.error("claude 命令未找到, 请确保已安装 Claude Code")
92
- return None
93
-
94
-
95
65
  def send_error_email(email_channel, from_email: str, subject: str, body: str,
96
66
  references: str, in_reply_to: str) -> bool:
97
67
  """发送错误通知邮件 (subject 加 Re: 前缀)。
@@ -316,13 +286,22 @@ class ConversationHandler:
316
286
  # ------------------------------------------------------------------ #
317
287
 
318
288
  def _build_prompt(self, session_file_path: str) -> str:
319
- """构建极简 prompt, 让 Claude 自助读 session 文件。"""
289
+ """构建极简 prompt, 让 Claude 自助读 session 文件。
290
+
291
+ 末尾显式约束: session 文件里的 from/to/subject 字段只是上下文,
292
+ 不要被 Claude 当成"邮件头模板"复述进回复正文; 也不要在末尾署名邮箱
293
+ —— 这些都已经由 SMTP MIME header 处理, 正文里复述只会冗余。
294
+ """
320
295
  return (
321
296
  f"用户最新邮件已写入 session 文件: {session_file_path}\n\n"
322
297
  "请用 Read 工具读取该文件, 了解完整对话上下文 "
323
298
  "(emails 字段是邮件列表, direction=incoming/outgoing), "
324
299
  "然后回复用户最新邮件。\n\n"
325
300
  "回复内容将作为邮件正文发送, 请用纯文本格式。"
301
+ "不要在正文里复述「发件人 / From」「收件人 / To」「主题 / Subject」"
302
+ "等邮件头字段(session 文件里的 from/to/subject 仅供上下文参考), "
303
+ "也不要在末尾署名或附上任何邮箱地址 — "
304
+ "这些会由邮件系统自动添加。"
326
305
  )
327
306
 
328
307
  # ------------------------------------------------------------------ #
@@ -444,7 +423,7 @@ class ConversationHandler:
444
423
  session_path = str(self._session_path(session_id))
445
424
  prompt = self._build_prompt(session_path)
446
425
  cwd = session.get("cwd") or str(Path.home())
447
- response = call_claude(prompt, cwd=cwd)
426
+ response = cr_module.call_claude(prompt, cwd=cwd)
448
427
 
449
428
  # 7. claude 失败 → 写日志 + 发邮件通知用户
450
429
  if response is None: