dsh-email 0.10.2 → 0.10.3
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.
- package/README.en.md +5 -1
- package/README.md +5 -1
- package/lib/approval.d.ts +34 -0
- package/lib/approval.js +55 -0
- package/lib/index.d.ts +8 -12
- package/lib/index.js +17 -669
- package/lib/mail-client.d.ts +12 -10
- package/lib/mail-client.js +141 -52
- package/lib/runtime.d.ts +36 -0
- package/lib/runtime.js +92 -0
- package/lib/tool-contract.d.ts +423 -0
- package/lib/tool-contract.js +353 -0
- package/lib/tools.d.ts +13 -0
- package/lib/tools.js +191 -0
- package/package.json +1 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
/** Model-facing parameters, input normalization, output schemas and text rendering. */
|
|
2
|
+
export const MAX_LIMIT = 100;
|
|
3
|
+
export const ACCOUNT_HINT = '账号名(配置了 accounts 多个账号时选择),省略时用 defaultAccount。可用账号见 email_folders 的报错或插件 README';
|
|
4
|
+
/** Parse date-only or ISO input; an inclusive end date maps to the next UTC day. */
|
|
5
|
+
export function parseEmailDay(input, label, endInclusive = false) {
|
|
6
|
+
const text = input.trim();
|
|
7
|
+
let date = null;
|
|
8
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(text)) {
|
|
9
|
+
date = new Date(text + 'T00:00:00Z');
|
|
10
|
+
}
|
|
11
|
+
else {
|
|
12
|
+
const parsed = new Date(text);
|
|
13
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
14
|
+
date = parsed;
|
|
15
|
+
}
|
|
16
|
+
if (date === null || Number.isNaN(date.getTime())) {
|
|
17
|
+
throw new Error(label + ' 不是有效日期,请给形如 2026-08-01 或 2026-08-01T00:00:00Z 的值');
|
|
18
|
+
}
|
|
19
|
+
if (endInclusive)
|
|
20
|
+
return new Date(date.getTime() + 24 * 3600 * 1000);
|
|
21
|
+
return date;
|
|
22
|
+
}
|
|
23
|
+
export function executionSignal(exec) {
|
|
24
|
+
if (typeof exec !== 'object' || exec === null)
|
|
25
|
+
return undefined;
|
|
26
|
+
const signal = exec.signal;
|
|
27
|
+
return signal instanceof AbortSignal ? signal : undefined;
|
|
28
|
+
}
|
|
29
|
+
export function compileParameters(spec) {
|
|
30
|
+
const properties = {};
|
|
31
|
+
const required = [];
|
|
32
|
+
for (const [key, prop] of Object.entries(spec)) {
|
|
33
|
+
if (prop?.required === true)
|
|
34
|
+
required.push(key);
|
|
35
|
+
const node = {};
|
|
36
|
+
if (typeof prop?.type === 'string')
|
|
37
|
+
node.type = prop.type;
|
|
38
|
+
if (typeof prop?.description === 'string')
|
|
39
|
+
node.description = prop.description;
|
|
40
|
+
if (prop?.type === 'array' && prop.items !== null && typeof prop.items === 'object') {
|
|
41
|
+
const items = { type: 'string' };
|
|
42
|
+
if (prop.items.type === 'object')
|
|
43
|
+
items.additionalProperties = true;
|
|
44
|
+
node.items = items;
|
|
45
|
+
}
|
|
46
|
+
properties[key] = node;
|
|
47
|
+
}
|
|
48
|
+
return { type: 'object', properties, ...(required.length > 0 ? { required } : {}) };
|
|
49
|
+
}
|
|
50
|
+
export function normalizeAttachmentPaths(value) {
|
|
51
|
+
if (value === undefined || value === null)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (!Array.isArray(value) || value.some(path => typeof path !== 'string' || path.trim() === '')) {
|
|
54
|
+
throw new Error('attachments 必须是文件路径字符串数组(且不能包含空字符串)');
|
|
55
|
+
}
|
|
56
|
+
return value.map(path => path.trim());
|
|
57
|
+
}
|
|
58
|
+
export const strArray = { type: 'array', items: { type: 'string' } };
|
|
59
|
+
export const addrArray = { type: 'array', items: { type: 'object', additionalProperties: true } };
|
|
60
|
+
export const messageShape = {
|
|
61
|
+
uid: { type: 'integer' },
|
|
62
|
+
date: { type: 'string' },
|
|
63
|
+
from: addrArray,
|
|
64
|
+
subject: { type: 'string' },
|
|
65
|
+
seen: { type: 'boolean' },
|
|
66
|
+
flagged: { type: 'boolean' },
|
|
67
|
+
size: { type: 'integer' },
|
|
68
|
+
hasAttachments: { type: 'boolean' },
|
|
69
|
+
};
|
|
70
|
+
export const listSchema = {
|
|
71
|
+
type: 'object',
|
|
72
|
+
properties: {
|
|
73
|
+
account: { type: 'string' },
|
|
74
|
+
count: { type: 'integer' },
|
|
75
|
+
folder: { type: 'string' },
|
|
76
|
+
messages: { type: 'array', items: { type: 'object', properties: messageShape, additionalProperties: true } },
|
|
77
|
+
},
|
|
78
|
+
additionalProperties: true,
|
|
79
|
+
};
|
|
80
|
+
export const readSchema = {
|
|
81
|
+
type: 'object',
|
|
82
|
+
properties: {
|
|
83
|
+
account: { type: 'string' },
|
|
84
|
+
uid: { type: 'integer' },
|
|
85
|
+
folder: { type: 'string' },
|
|
86
|
+
date: { type: 'string' },
|
|
87
|
+
from: addrArray,
|
|
88
|
+
to: addrArray,
|
|
89
|
+
cc: addrArray,
|
|
90
|
+
subject: { type: 'string' },
|
|
91
|
+
text: { type: 'string' },
|
|
92
|
+
attachments: { type: 'array', items: { type: 'object', additionalProperties: true } },
|
|
93
|
+
truncated: { type: 'boolean' },
|
|
94
|
+
},
|
|
95
|
+
additionalProperties: true,
|
|
96
|
+
};
|
|
97
|
+
export const sendSchema = {
|
|
98
|
+
type: 'object',
|
|
99
|
+
properties: {
|
|
100
|
+
account: { type: 'string' },
|
|
101
|
+
messageId: { type: 'string' },
|
|
102
|
+
accepted: strArray,
|
|
103
|
+
rejected: strArray,
|
|
104
|
+
response: { type: 'string' },
|
|
105
|
+
},
|
|
106
|
+
additionalProperties: true,
|
|
107
|
+
};
|
|
108
|
+
export const foldersSchema = {
|
|
109
|
+
type: 'object',
|
|
110
|
+
properties: {
|
|
111
|
+
account: { type: 'string' },
|
|
112
|
+
folders: {
|
|
113
|
+
type: 'array',
|
|
114
|
+
items: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
name: { type: 'string' },
|
|
118
|
+
path: { type: 'string' },
|
|
119
|
+
specialUse: { type: 'string' },
|
|
120
|
+
subscribed: { type: 'boolean' },
|
|
121
|
+
},
|
|
122
|
+
additionalProperties: true,
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
additionalProperties: true,
|
|
127
|
+
};
|
|
128
|
+
export const attachmentSchema = {
|
|
129
|
+
type: 'object',
|
|
130
|
+
properties: {
|
|
131
|
+
account: { type: 'string' },
|
|
132
|
+
uid: { type: 'integer' },
|
|
133
|
+
filename: { type: 'string' },
|
|
134
|
+
contentType: { type: 'string' },
|
|
135
|
+
size: { type: 'integer' },
|
|
136
|
+
path: { type: 'string' },
|
|
137
|
+
},
|
|
138
|
+
additionalProperties: true,
|
|
139
|
+
};
|
|
140
|
+
export const markSchema = {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
account: { type: 'string' },
|
|
144
|
+
uid: { type: 'integer' },
|
|
145
|
+
folder: { type: 'string' },
|
|
146
|
+
action: { type: 'string' },
|
|
147
|
+
seen: { type: 'boolean' },
|
|
148
|
+
flagged: { type: 'boolean' },
|
|
149
|
+
movedTo: { type: 'string' },
|
|
150
|
+
movedUid: { type: 'integer' },
|
|
151
|
+
},
|
|
152
|
+
additionalProperties: true,
|
|
153
|
+
};
|
|
154
|
+
export const MARK_ACTIONS = ['read', 'unread', 'star', 'unstar', 'move'];
|
|
155
|
+
export const REPLY_MODES = ['reply', 'reply-all', 'forward'];
|
|
156
|
+
export const MARK_LABELS = {
|
|
157
|
+
read: '标记为已读',
|
|
158
|
+
unread: '标记为未读',
|
|
159
|
+
star: '加星标',
|
|
160
|
+
unstar: '取消星标',
|
|
161
|
+
move: '移动',
|
|
162
|
+
};
|
|
163
|
+
export const REPLY_LABELS = {
|
|
164
|
+
reply: '回复',
|
|
165
|
+
'reply-all': '回复全部',
|
|
166
|
+
forward: '转发',
|
|
167
|
+
};
|
|
168
|
+
export function oneText(text) {
|
|
169
|
+
return [{ type: 'text', text }];
|
|
170
|
+
}
|
|
171
|
+
export function describeMessage(message) {
|
|
172
|
+
const from = message.from.map(a => a.name ?? a.address).filter(Boolean).join(', ') || '(未知)';
|
|
173
|
+
const flags = [
|
|
174
|
+
message.seen ? '' : '未读',
|
|
175
|
+
message.flagged ? '已标星' : '',
|
|
176
|
+
message.hasAttachments ? '含附件' : '',
|
|
177
|
+
].filter(Boolean);
|
|
178
|
+
const parts = ['uid=' + message.uid, from, message.date];
|
|
179
|
+
if (flags.length > 0)
|
|
180
|
+
parts.push(flags.join('、'));
|
|
181
|
+
return (message.subject || '(无主题)') + ' [' + parts.join(' | ') + ']';
|
|
182
|
+
}
|
|
183
|
+
export function renderList(value) {
|
|
184
|
+
if (value.messages.length === 0) {
|
|
185
|
+
return oneText('账号 ' + value.account + ',文件夹 "' + value.folder + '" 共 ' + value.count + ' 封邮件,本次没有要列出的邮件。');
|
|
186
|
+
}
|
|
187
|
+
const lines = value.messages.map((m, i) => '#' + (i + 1) + ' ' + describeMessage(m));
|
|
188
|
+
return oneText('账号 ' + value.account + ',文件夹 "' + value.folder + '" 共 ' + value.count + ' 封邮件,最新 ' + value.messages.length + ' 封:\n\n' + lines.join('\n') + '\n\n用 email_read 配合 uid 阅读全文。');
|
|
189
|
+
}
|
|
190
|
+
export function renderRead(value) {
|
|
191
|
+
const from = value.from.map(a => a.name ?? a.address).filter(Boolean).join(', ') || '(未知)';
|
|
192
|
+
const attach = value.attachments.length > 0
|
|
193
|
+
? '\n附件:' + value.attachments.map((a, i) => '#' + i + ' ' + a.filename + '(' + a.contentType + ',' + a.size + ' 字节)').join(';') + '\n(用 email_attachment 配合 uid 与序号下载)'
|
|
194
|
+
: '';
|
|
195
|
+
return oneText('账号 ' + value.account + ',主题:' + (value.subject || '(无主题)') + '\n来自:' + from + '\n时间:' + (value.date || '(未知)') + attach + '\n\n' + value.text);
|
|
196
|
+
}
|
|
197
|
+
export function renderSearch(value) {
|
|
198
|
+
if (value.messages.length === 0) {
|
|
199
|
+
return oneText('账号 ' + value.account + ',在文件夹 "' + value.folder + '" 中搜索 "' + value.query + '":共 ' + value.count + ' 条匹配,本次没有列出。');
|
|
200
|
+
}
|
|
201
|
+
const lines = value.messages.map((m, i) => '#' + (i + 1) + ' ' + describeMessage(m));
|
|
202
|
+
return oneText('账号 ' + value.account + ',在文件夹 "' + value.folder + '" 中搜索 "' + value.query + '":共 ' + value.count + ' 条匹配,展示最新 ' + value.messages.length + ' 条:\n\n' + lines.join('\n'));
|
|
203
|
+
}
|
|
204
|
+
export function renderSend(value) {
|
|
205
|
+
const rejected = value.rejected.length > 0 ? ';被拒:' + value.rejected.join(', ') : '';
|
|
206
|
+
return oneText('账号 ' + value.account + ' 邮件已发送,messageId: ' + value.messageId + ';成功送达:' + value.accepted.join(', ') + rejected + ';服务器响应:' + value.response);
|
|
207
|
+
}
|
|
208
|
+
export function renderFolders(value) {
|
|
209
|
+
if (value.folders.length === 0)
|
|
210
|
+
return oneText('账号 ' + value.account + ':未列出任何文件夹。');
|
|
211
|
+
const lines = value.folders.map((f, i) => '#' + (i + 1) + ' ' + f.path + (f.specialUse !== '' ? ' [' + f.specialUse + ']' : '') + (f.subscribed ? '' : '(未订阅)'));
|
|
212
|
+
return oneText('账号 ' + value.account + ' 的文件夹(' + value.folders.length + ' 个):\n\n' + lines.join('\n') + '\n\n把 folder 参数填成其中的 path 即可。');
|
|
213
|
+
}
|
|
214
|
+
export function renderAttachment(value) {
|
|
215
|
+
return oneText('账号 ' + value.account + ' 已下载附件 "' + value.filename + '"(' + value.contentType + ',' + value.size + ' 字节)到:\n' + value.path + '\n可用 read 工具读取该文件。');
|
|
216
|
+
}
|
|
217
|
+
export function renderMark(value) {
|
|
218
|
+
let text = '账号 ' + value.account + ':文件夹 "' + value.folder + '" 中 uid=' + value.uid + ' 已' + (MARK_LABELS[value.action] ?? value.action);
|
|
219
|
+
if (value.action === 'move') {
|
|
220
|
+
text += '到 "' + (value.movedTo ?? '') + '"' + (typeof value.movedUid === 'number' ? '(新 uid=' + value.movedUid + ')' : '');
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
text += '(当前:' + (value.seen ? '已读' : '未读') + (value.flagged ? '、已标星' : '') + ')';
|
|
224
|
+
}
|
|
225
|
+
return oneText(text);
|
|
226
|
+
}
|
|
227
|
+
export function renderReply(value) {
|
|
228
|
+
const rejected = value.rejected.length > 0 ? ';被拒:' + value.rejected.join(', ') : '';
|
|
229
|
+
return oneText('账号 ' + value.account + ' 已' + REPLY_LABELS[value.mode] + ' uid=' + value.originalUid + ' 的邮件:收件人 ' + value.to.join(', ') + ',主题「' + value.subject + '」,messageId: ' + value.messageId + rejected);
|
|
230
|
+
}
|
|
231
|
+
export function renderWatch(value) {
|
|
232
|
+
if (value.firstRun) {
|
|
233
|
+
return oneText('账号 ' + value.account + ':已建立新邮件监视基线(当前未读 ' + value.totalUnread + ' 封)。之后调用 email_watch 只会报告新到的邮件。');
|
|
234
|
+
}
|
|
235
|
+
if (value.newCount === 0) {
|
|
236
|
+
return oneText('账号 ' + value.account + ':没有新邮件(当前未读 ' + value.totalUnread + ' 封)。');
|
|
237
|
+
}
|
|
238
|
+
const lines = value.messages.map((m, i) => '#' + (i + 1) + ' ' + describeMessage(m));
|
|
239
|
+
return oneText('账号 ' + value.account + ' 有 ' + value.newCount + ' 封新邮件:\n\n' + lines.join('\n') + '\n\n用 email_read 配合 uid 阅读全文。');
|
|
240
|
+
}
|
|
241
|
+
export const renderHealth = (_args, value) => {
|
|
242
|
+
const rec = (value ?? {});
|
|
243
|
+
const rawChecks = Array.isArray(rec.checks) ? rec.checks : [];
|
|
244
|
+
const lines = ['dsh-email 自检' + (rec.ok === true ? ':正常。' : ':发现问题。')];
|
|
245
|
+
for (const item of rawChecks) {
|
|
246
|
+
const c = (item ?? {});
|
|
247
|
+
lines.push('- ' + String(c.name) + ':' + (c.ok === true ? '✅ ' + String(c.detail ?? '') : '❌ ' + String(c.detail ?? '')));
|
|
248
|
+
}
|
|
249
|
+
return [{ type: 'text', text: lines.join('\n') }];
|
|
250
|
+
};
|
|
251
|
+
export const replySchema = {
|
|
252
|
+
type: 'object',
|
|
253
|
+
properties: {
|
|
254
|
+
account: { type: 'string' },
|
|
255
|
+
mode: { type: 'string' },
|
|
256
|
+
originalUid: { type: 'integer' },
|
|
257
|
+
messageId: { type: 'string' },
|
|
258
|
+
accepted: strArray,
|
|
259
|
+
rejected: strArray,
|
|
260
|
+
response: { type: 'string' },
|
|
261
|
+
to: strArray,
|
|
262
|
+
subject: { type: 'string' },
|
|
263
|
+
},
|
|
264
|
+
additionalProperties: true,
|
|
265
|
+
};
|
|
266
|
+
export const watchSchema = {
|
|
267
|
+
type: 'object',
|
|
268
|
+
properties: {
|
|
269
|
+
account: { type: 'string' },
|
|
270
|
+
folder: { type: 'string' },
|
|
271
|
+
firstRun: { type: 'boolean' },
|
|
272
|
+
newCount: { type: 'integer' },
|
|
273
|
+
totalUnread: { type: 'integer' },
|
|
274
|
+
messages: { type: 'array', items: { type: 'object', properties: messageShape, additionalProperties: true } },
|
|
275
|
+
},
|
|
276
|
+
additionalProperties: true,
|
|
277
|
+
};
|
|
278
|
+
export const descriptions = {
|
|
279
|
+
"email_list": 'List recent emails in a mailbox folder (newest first). Returns uid, date, sender, subject and flags without message bodies; use email_read with a uid to fetch the full text. Optional since/until (dates like 2026-08-01) filter by received date.',
|
|
280
|
+
"email_read": 'Read one full email message by its uid (from email_list or email_search). Returns the plain-text body (HTML mail is converted; oversized bodies are truncated) plus attachment metadata; use email_attachment to download one.',
|
|
281
|
+
"email_mark": 'Change an existing message: mark it read/unread, star/unstar it, or move it to another folder. Use after email_list/email_search when the user wants to tidy the mailbox (archive, clear unread, flag important mail). Moving uses the server MOVE/COPY so the uid changes; the new uid is reported when the server provides it.',
|
|
282
|
+
"email_search": 'Search emails by a keyword. The server first searches sender, recipients and subject; when that finds nothing and bodySearchFallback is enabled, recent messages are scanned locally including their body. since/until still constrain both paths. Returns the same compact rows as email_list.',
|
|
283
|
+
"email_send": 'Send an email from a configured account, optionally with file attachments (absolute paths, or relative to the dsh process cwd). Sending asks the user for approval (recipient, subject and attachment count are shown) unless sendApproval is disabled; in Full Access mode the approval policy never asks, so the send is refused with an explanation instead. Never invent recipients or content without the user\'s instruction.',
|
|
284
|
+
"email_reply": 'Reply to, reply-all to, or forward an existing message (mode: reply | reply-all | forward). Recipients come from the original message (your own address is excluded automatically), the subject gets a single Re:/Fwd: prefix, the original text is quoted underneath, and In-Reply-To/References headers keep mail clients threading correctly. mode=forward needs the to parameter. Like email_send, this asks the user for approval before sending. Never invent recipients or content without the user\'s instruction.',
|
|
285
|
+
"email_folders": 'List the mailbox folders of an account (INBOX, Sent, Trash, custom folders, ...). Use the returned path values as the folder argument of the other email tools.',
|
|
286
|
+
"email_health": 'Self-check for dsh-email: summarizes configured accounts (provider / IMAP / SMTP hosts) without any network connection and never shows passwords. Run this first when troubleshooting.',
|
|
287
|
+
"email_attachment": 'Download one attachment of a message to a local file (size capped by maxAttachmentBytes). The index matches the attachments array of email_read. Returns the absolute path of the written file.',
|
|
288
|
+
"email_watch": 'Check for NEW unread emails since the last check (cursor-based). The first call per account+folder sets the baseline and reports nothing as new; every later call returns only unseen unread messages. Call this periodically (e.g. via a scheduled task) to notify the user about new mail. Returns newCount, the new messages, and totalUnread.'
|
|
289
|
+
};
|
|
290
|
+
export const parameters = {
|
|
291
|
+
"email_list": compileParameters({
|
|
292
|
+
folder: { type: 'string', description: 'IMAP folder path (see email_folders); defaults to the account inboxFolder' },
|
|
293
|
+
limit: { type: 'integer', description: 'How many messages to return, 1-100, default 20' },
|
|
294
|
+
offset: { type: 'integer', description: 'Skip this many newest messages first, default 0' },
|
|
295
|
+
unreadOnly: { type: 'boolean', description: 'Only list unread messages, default false' },
|
|
296
|
+
since: { type: 'string', description: 'Only list messages received on or after this date, e.g. 2026-08-01 (optional)' },
|
|
297
|
+
until: { type: 'string', description: 'Only list messages received on or before this date, e.g. 2026-08-26 (optional)' },
|
|
298
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
299
|
+
}),
|
|
300
|
+
"email_read": compileParameters({
|
|
301
|
+
uid: { type: 'integer', required: true, description: 'Message uid from email_list or email_search' },
|
|
302
|
+
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
303
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
304
|
+
}),
|
|
305
|
+
"email_mark": compileParameters({
|
|
306
|
+
uid: { type: 'integer', required: true, description: 'Message uid from email_list or email_search' },
|
|
307
|
+
action: { type: 'string', required: true, description: 'What to do: read, unread, star, unstar, or move' },
|
|
308
|
+
toFolder: { type: 'string', description: 'Destination folder path for action=move (see email_folders for valid paths)' },
|
|
309
|
+
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
310
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
311
|
+
}),
|
|
312
|
+
"email_search": compileParameters({
|
|
313
|
+
query: { type: 'string', required: true, description: 'Keyword to search for' },
|
|
314
|
+
folder: { type: 'string', description: 'IMAP folder to search in; defaults to the account inboxFolder' },
|
|
315
|
+
limit: { type: 'integer', description: 'How many matches to return, 1-100, default 10' },
|
|
316
|
+
since: { type: 'string', description: 'Only search messages received on or after this date, e.g. 2026-08-01 (optional)' },
|
|
317
|
+
until: { type: 'string', description: 'Only search messages received on or before this date, e.g. 2026-08-26 (optional)' },
|
|
318
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
319
|
+
}),
|
|
320
|
+
"email_send": compileParameters({
|
|
321
|
+
to: { type: 'string', required: true, description: 'Recipient(s), comma-separated' },
|
|
322
|
+
subject: { type: 'string', required: true, description: 'Email subject' },
|
|
323
|
+
text: { type: 'string', description: 'Plain-text body' },
|
|
324
|
+
cc: { type: 'string', description: 'CC recipient(s), comma-separated' },
|
|
325
|
+
attachments: { type: 'array', items: { type: 'string' }, description: 'File paths to attach (absolute, or relative to the dsh process cwd)' },
|
|
326
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
327
|
+
}),
|
|
328
|
+
"email_reply": compileParameters({
|
|
329
|
+
uid: { type: 'integer', required: true, description: 'Message uid to answer/forward, from email_list or email_search' },
|
|
330
|
+
text: { type: 'string', required: true, description: 'The new text to write; the original is quoted below it automatically' },
|
|
331
|
+
mode: { type: 'string', description: 'reply (default), reply-all, or forward' },
|
|
332
|
+
to: { type: 'string', description: 'Recipient(s) for mode=forward, comma-separated' },
|
|
333
|
+
cc: { type: 'string', description: 'Extra CC recipient(s), comma-separated (optional)' },
|
|
334
|
+
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
335
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
336
|
+
}),
|
|
337
|
+
"email_folders": compileParameters({
|
|
338
|
+
subscribedOnly: { type: 'boolean', description: 'Only subscribed folders, default false' },
|
|
339
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
340
|
+
}),
|
|
341
|
+
"email_health": compileParameters({}),
|
|
342
|
+
"email_attachment": compileParameters({
|
|
343
|
+
uid: { type: 'integer', required: true, description: 'Message uid from email_list or email_search' },
|
|
344
|
+
index: { type: 'integer', description: '0-based attachment index, as listed by email_read; default 0' },
|
|
345
|
+
folder: { type: 'string', description: 'IMAP folder the uid belongs to; defaults to the account inboxFolder' },
|
|
346
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
347
|
+
}),
|
|
348
|
+
"email_watch": compileParameters({
|
|
349
|
+
folder: { type: 'string', description: 'IMAP folder path (see email_folders); defaults to the account inboxFolder' },
|
|
350
|
+
limit: { type: 'integer', description: 'Max number of new messages to return, 1-100, default 20' },
|
|
351
|
+
account: { type: 'string', description: ACCOUNT_HINT },
|
|
352
|
+
})
|
|
353
|
+
};
|
package/lib/tools.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EmailRuntime } from './runtime.js';
|
|
2
|
+
import { type TextBlock } from './tool-contract.js';
|
|
3
|
+
export interface EmailToolDefinition {
|
|
4
|
+
name: string;
|
|
5
|
+
description: string;
|
|
6
|
+
parameters: Record<string, unknown>;
|
|
7
|
+
output: {
|
|
8
|
+
schema: Record<string, unknown>;
|
|
9
|
+
render(args: unknown, value: unknown): TextBlock[];
|
|
10
|
+
};
|
|
11
|
+
execute(args: unknown, exec?: unknown): Promise<unknown>;
|
|
12
|
+
}
|
|
13
|
+
export declare function buildEmailTools(runtime: Pick<EmailRuntime, 'getPool' | 'getEffectiveSettings' | 'watch'>): EmailToolDefinition[];
|
package/lib/tools.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
import { clampInt, PROVIDER_PRESETS } from './config.js';
|
|
2
|
+
import { messageOf } from './mail-client.js';
|
|
3
|
+
import { descriptions, parameters, MAX_LIMIT, MARK_ACTIONS, REPLY_MODES, executionSignal, normalizeAttachmentPaths, parseEmailDay, attachmentSchema, foldersSchema, listSchema, markSchema, readSchema, replySchema, sendSchema, watchSchema, renderAttachment, renderFolders, renderHealth, renderList, renderMark, renderRead, renderReply, renderSearch, renderSend, renderWatch, } from './tool-contract.js';
|
|
4
|
+
export function buildEmailTools(runtime) {
|
|
5
|
+
const { getPool, getEffectiveSettings, watch: watchCore } = runtime;
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
name: 'email_list',
|
|
9
|
+
description: descriptions.email_list,
|
|
10
|
+
parameters: parameters.email_list,
|
|
11
|
+
output: {
|
|
12
|
+
schema: listSchema,
|
|
13
|
+
render: (_args, value) => renderList(value),
|
|
14
|
+
},
|
|
15
|
+
async execute(rawArgs, exec) {
|
|
16
|
+
const args = rawArgs;
|
|
17
|
+
const limit = clampInt(args.limit, 20, 1, MAX_LIMIT);
|
|
18
|
+
const offset = clampInt(args.offset, 0, 0, 10000);
|
|
19
|
+
const since = args.since?.trim() ? parseEmailDay(args.since, 'since') : undefined;
|
|
20
|
+
const until = args.until?.trim() ? parseEmailDay(args.until, 'until', true) : undefined;
|
|
21
|
+
return await getPool().list(args.account, args.folder?.trim() || '', limit, offset, args.unreadOnly === true, since, until, executionSignal(exec));
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
name: 'email_read',
|
|
26
|
+
description: descriptions.email_read,
|
|
27
|
+
parameters: parameters.email_read,
|
|
28
|
+
output: {
|
|
29
|
+
schema: readSchema,
|
|
30
|
+
render: (_args, value) => renderRead(value),
|
|
31
|
+
},
|
|
32
|
+
async execute(rawArgs, exec) {
|
|
33
|
+
const args = rawArgs;
|
|
34
|
+
if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
|
|
35
|
+
throw new Error('uid 必须是正整数(用 email_list 获取)');
|
|
36
|
+
}
|
|
37
|
+
return await getPool().read(args.account, args.uid, args.folder?.trim() || '', executionSignal(exec));
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
name: 'email_mark',
|
|
42
|
+
description: descriptions.email_mark,
|
|
43
|
+
parameters: parameters.email_mark,
|
|
44
|
+
output: {
|
|
45
|
+
schema: markSchema,
|
|
46
|
+
render: (_args, value) => renderMark(value),
|
|
47
|
+
},
|
|
48
|
+
async execute(rawArgs, exec) {
|
|
49
|
+
const args = rawArgs;
|
|
50
|
+
if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
|
|
51
|
+
throw new Error('uid 必须是正整数(用 email_list 获取)');
|
|
52
|
+
}
|
|
53
|
+
const action = (typeof args.action === 'string' ? args.action.trim().toLowerCase() : '');
|
|
54
|
+
if (!MARK_ACTIONS.includes(action)) {
|
|
55
|
+
throw new Error('action 必须是 ' + MARK_ACTIONS.join('、') + ' 之一');
|
|
56
|
+
}
|
|
57
|
+
if (action === 'move' && (typeof args.toFolder !== 'string' || args.toFolder.trim() === '')) {
|
|
58
|
+
throw new Error('action=move 时需要 toFolder 参数(用 email_folders 查看可用文件夹)');
|
|
59
|
+
}
|
|
60
|
+
return await getPool().mark(args.account, args.folder?.trim() || '', args.uid, action, args.toFolder, executionSignal(exec));
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'email_search',
|
|
65
|
+
description: descriptions.email_search,
|
|
66
|
+
parameters: parameters.email_search,
|
|
67
|
+
output: {
|
|
68
|
+
schema: listSchema,
|
|
69
|
+
render: (_args, value) => renderSearch(value),
|
|
70
|
+
},
|
|
71
|
+
async execute(rawArgs, exec) {
|
|
72
|
+
const args = rawArgs;
|
|
73
|
+
if (typeof args.query !== 'string' || args.query.trim() === '')
|
|
74
|
+
throw new Error('query 不能为空');
|
|
75
|
+
const limit = clampInt(args.limit, 10, 1, MAX_LIMIT);
|
|
76
|
+
const since = args.since?.trim() ? parseEmailDay(args.since, 'since') : undefined;
|
|
77
|
+
const until = args.until?.trim() ? parseEmailDay(args.until, 'until', true) : undefined;
|
|
78
|
+
return await getPool().search(args.account, args.query.trim(), args.folder?.trim() || '', limit, since, until, executionSignal(exec));
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: 'email_send',
|
|
83
|
+
description: descriptions.email_send,
|
|
84
|
+
parameters: parameters.email_send,
|
|
85
|
+
output: {
|
|
86
|
+
schema: sendSchema,
|
|
87
|
+
render: (_args, value) => renderSend(value),
|
|
88
|
+
},
|
|
89
|
+
async execute(rawArgs, exec) {
|
|
90
|
+
const args = rawArgs;
|
|
91
|
+
if (typeof args.to !== 'string' || args.to.trim() === '')
|
|
92
|
+
throw new Error('to 不能为空');
|
|
93
|
+
if (typeof args.subject !== 'string' || args.subject.trim() === '')
|
|
94
|
+
throw new Error('subject 不能为空');
|
|
95
|
+
return await getPool().send(args.account, args.to.trim(), args.subject.trim(), typeof args.text === 'string' ? args.text : undefined, typeof args.cc === 'string' && args.cc.trim() !== '' ? args.cc.trim() : undefined, normalizeAttachmentPaths(args.attachments), executionSignal(exec));
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
name: 'email_reply',
|
|
100
|
+
description: descriptions.email_reply,
|
|
101
|
+
parameters: parameters.email_reply,
|
|
102
|
+
output: { schema: replySchema, render: (_args, value) => renderReply(value) },
|
|
103
|
+
async execute(rawArgs, exec) {
|
|
104
|
+
const args = rawArgs;
|
|
105
|
+
if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
|
|
106
|
+
throw new Error('uid 必须是正整数(用 email_list 获取)');
|
|
107
|
+
}
|
|
108
|
+
if (typeof args.text !== 'string' || args.text.trim() === '')
|
|
109
|
+
throw new Error('text 不能为空');
|
|
110
|
+
const mode = ((typeof args.mode === 'string' && args.mode.trim() !== '' ? args.mode.trim().toLowerCase() : 'reply'));
|
|
111
|
+
if (!REPLY_MODES.includes(mode)) {
|
|
112
|
+
throw new Error('mode 必须是 ' + REPLY_MODES.join('、') + ' 之一');
|
|
113
|
+
}
|
|
114
|
+
if (mode === 'forward' && (typeof args.to !== 'string' || args.to.trim() === '')) {
|
|
115
|
+
throw new Error('mode=forward 时需要 to 参数指定转发收件人');
|
|
116
|
+
}
|
|
117
|
+
const cc = typeof args.cc === 'string' && args.cc.trim() !== '' ? args.cc.trim() : undefined;
|
|
118
|
+
return await getPool().reply(args.account, args.folder?.trim() || '', args.uid, mode, args.text, args.to?.trim() ?? '', cc, executionSignal(exec));
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
name: 'email_folders',
|
|
123
|
+
description: descriptions.email_folders,
|
|
124
|
+
parameters: parameters.email_folders,
|
|
125
|
+
output: {
|
|
126
|
+
schema: foldersSchema,
|
|
127
|
+
render: (_args, value) => renderFolders(value),
|
|
128
|
+
},
|
|
129
|
+
async execute(rawArgs, exec) {
|
|
130
|
+
const args = rawArgs;
|
|
131
|
+
return await getPool().folders(args.account, args.subscribedOnly === true, executionSignal(exec));
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: 'email_health',
|
|
136
|
+
description: descriptions.email_health,
|
|
137
|
+
parameters: parameters.email_health,
|
|
138
|
+
output: { schema: { type: 'object', additionalProperties: true }, render: renderHealth },
|
|
139
|
+
async execute(_rawArgs, exec) {
|
|
140
|
+
executionSignal(exec)?.throwIfAborted();
|
|
141
|
+
const checks = [];
|
|
142
|
+
try {
|
|
143
|
+
const effective = getEffectiveSettings();
|
|
144
|
+
const entries = [...effective.accounts.entries()];
|
|
145
|
+
for (const [accountName, account] of entries.slice(0, 8)) {
|
|
146
|
+
const provider = Object.entries(PROVIDER_PRESETS).find(([, preset]) => (preset.imap.host === account.imap.host && preset.smtp.host === account.smtp.host))?.[0] ?? 'custom';
|
|
147
|
+
checks.push({
|
|
148
|
+
name: '账号 ' + accountName,
|
|
149
|
+
ok: true,
|
|
150
|
+
detail: provider + ' / ' + account.user + ' / IMAP ' + account.imap.host + ' / SMTP ' + account.smtp.host,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
return { ok: true, plugin: 'dsh-email', accountCount: entries.length, checks };
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
checks.push({ name: '邮箱配置', ok: false, detail: messageOf(error, '未配置邮箱账号') });
|
|
157
|
+
return { ok: false, plugin: 'dsh-email', accountCount: 0, checks };
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: 'email_attachment',
|
|
163
|
+
description: descriptions.email_attachment,
|
|
164
|
+
parameters: parameters.email_attachment,
|
|
165
|
+
output: {
|
|
166
|
+
schema: attachmentSchema,
|
|
167
|
+
render: (_args, value) => renderAttachment(value),
|
|
168
|
+
},
|
|
169
|
+
async execute(rawArgs, exec) {
|
|
170
|
+
const args = rawArgs;
|
|
171
|
+
if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
|
|
172
|
+
throw new Error('uid 必须是正整数(用 email_list 获取)');
|
|
173
|
+
}
|
|
174
|
+
const index = clampInt(args.index, 0, 0, 999);
|
|
175
|
+
const workspaceHint = typeof exec?.agent?.session?.header?.cwd === 'string' ? exec.agent.session.header.cwd : undefined;
|
|
176
|
+
return await getPool().downloadAttachment(args.account, args.folder?.trim() || '', args.uid, index, workspaceHint, executionSignal(exec));
|
|
177
|
+
}
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
name: 'email_watch',
|
|
181
|
+
description: descriptions.email_watch,
|
|
182
|
+
parameters: parameters.email_watch,
|
|
183
|
+
output: { schema: watchSchema, render: (_args, value) => renderWatch(value) },
|
|
184
|
+
async execute(rawArgs, exec) {
|
|
185
|
+
const args = rawArgs;
|
|
186
|
+
const limit = clampInt(args.limit, 20, 1, MAX_LIMIT);
|
|
187
|
+
return await watchCore(args.account?.trim() || '', args.folder?.trim() || '', limit, 'tool', executionSignal(exec));
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
];
|
|
191
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-email",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "IMAP/SMTP email tools for DeepSeek Harness: list, read, search and send mail, with QQ/163/126/Sina/Aliyun/Gmail/Outlook/iCloud presets.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|