mailcode 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.
- {mailcode-0.1.0 → mailcode-0.1.2}/PKG-INFO +1 -1
- mailcode-0.1.2/README.md +252 -0
- mailcode-0.1.2/mailcode/__init__.py +1 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/cli.py +130 -6
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/config.py +26 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/relay/conversation_handler.py +13 -34
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/relay/email_listener.py +189 -43
- mailcode-0.1.2/mailcode/relay/scheduler.py +816 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/relay/stateless_handler.py +9 -4
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/resources/default.json +12 -0
- mailcode-0.1.2/mailcode/schedule_cli.py +471 -0
- mailcode-0.1.2/mailcode/server.py +70 -0
- mailcode-0.1.2/mailcode/utils/claude_runner.py +43 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode.egg-info/PKG-INFO +1 -1
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode.egg-info/SOURCES.txt +3 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/pyproject.toml +1 -1
- mailcode-0.1.0/README.md +0 -151
- mailcode-0.1.0/mailcode/__init__.py +0 -1
- mailcode-0.1.0/mailcode/server.py +0 -40
- {mailcode-0.1.0 → mailcode-0.1.2}/LICENSE +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/channels/__init__.py +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/channels/email_channel.py +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/health.py +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/provider_presets.py +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/relay/__init__.py +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/relay/security.py +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/session_cli.py +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/utils/__init__.py +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode/utils/logging.py +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode.egg-info/dependency_links.txt +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode.egg-info/entry_points.txt +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/mailcode.egg-info/top_level.txt +0 -0
- {mailcode-0.1.0 → mailcode-0.1.2}/setup.cfg +0 -0
mailcode-0.1.2/README.md
ADDED
|
@@ -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.2"
|
|
@@ -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 主要用于:
|
|
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
|
|
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="
|
|
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:
|