dsh-email 0.11.0 → 0.13.0

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.
@@ -195,11 +195,24 @@ export function renderRead(value) {
195
195
  return oneText('账号 ' + value.account + ',主题:' + (value.subject || '(无主题)') + '\n来自:' + from + '\n时间:' + (value.date || '(未知)') + attach + '\n\n' + value.text);
196
196
  }
197
197
  export function renderSearch(value) {
198
+ const scanned = value.countKind === 'scanned';
199
+ // 回退扫描只看了最近 scannedLimit 封,不知道全文件夹匹配数:不能把本页条数说成「共 N 条」。
200
+ const scannedNote = '仅扫描最近 ' + (value.scannedLimit ?? 0) + ' 封,未统计全文件夹匹配数';
198
201
  if (value.messages.length === 0) {
202
+ if (scanned) {
203
+ return oneText('账号 ' + value.account + ',在文件夹 "' + value.folder + '" 中搜索 "' + value.query + '":本页没有匹配(' + scannedNote + ')。');
204
+ }
199
205
  return oneText('账号 ' + value.account + ',在文件夹 "' + value.folder + '" 中搜索 "' + value.query + '":共 ' + value.count + ' 条匹配,本次没有列出。');
200
206
  }
201
207
  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'));
208
+ const offset = value.offset ?? 0;
209
+ const window = offset > 0
210
+ ? '跳过最新 ' + offset + ' 条后展示 ' + value.messages.length + ' 条'
211
+ : '展示最新 ' + value.messages.length + ' 条';
212
+ const summary = scanned
213
+ ? '本页 ' + value.messages.length + ' 条(' + scannedNote + ')' + (offset > 0 ? ',已被 offset 跳过最新 ' + offset + ' 条' : '')
214
+ : '共 ' + value.count + ' 条匹配,' + window;
215
+ return oneText('账号 ' + value.account + ',在文件夹 "' + value.folder + '" 中搜索 "' + value.query + '":' + summary + ':\n\n' + lines.join('\n'));
203
216
  }
204
217
  export function renderSend(value) {
205
218
  const rejected = value.rejected.length > 0 ? ';被拒:' + value.rejected.join(', ') : '';
@@ -229,6 +242,9 @@ export function renderReply(value) {
229
242
  return oneText('账号 ' + value.account + ' 已' + REPLY_LABELS[value.mode] + ' uid=' + value.originalUid + ' 的邮件:收件人 ' + value.to.join(', ') + ',主题「' + value.subject + '」,messageId: ' + value.messageId + rejected);
230
243
  }
231
244
  export function renderWatch(value) {
245
+ if (value.reset === true) {
246
+ return oneText('账号 ' + value.account + ':文件夹 "' + value.folder + '" 的 UIDVALIDITY 已变化(服务器重新编号了邮件),已重新建立基线(当前未读 ' + value.totalUnread + ' 封)。这次不报告新邮件,之后照常。');
247
+ }
232
248
  if (value.firstRun) {
233
249
  return oneText('账号 ' + value.account + ':已建立新邮件监视基线(当前未读 ' + value.totalUnread + ' 封)。之后调用 email_watch 只会报告新到的邮件。');
234
250
  }
@@ -269,6 +285,7 @@ export const watchSchema = {
269
285
  account: { type: 'string' },
270
286
  folder: { type: 'string' },
271
287
  firstRun: { type: 'boolean' },
288
+ reset: { type: 'boolean' },
272
289
  newCount: { type: 'integer' },
273
290
  totalUnread: { type: 'integer' },
274
291
  messages: { type: 'array', items: { type: 'object', properties: messageShape, additionalProperties: true } },
@@ -279,7 +296,7 @@ export const descriptions = {
279
296
  "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
297
  "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
298
  "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; those hits are re-verified against the envelopes and, when none of them really carries the keyword (some servers answer every search with the same uids), recent messages are scanned locally including their body while bodySearchFallback is enabled. since/until still constrain both paths. Returns the same compact rows as email_list.',
299
+ "email_search": 'Search emails by a keyword. The server first searches sender, recipients and subject; those hits are re-verified against the envelopes and, when none of them really carries the keyword (some servers answer every search with the same uids), recent messages are scanned locally including their body while bodySearchFallback is enabled. offset skips the newest matches for paging; since/until still constrain both paths. Returns the same compact rows as email_list.',
283
300
  "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
301
  "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
302
  "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.',
@@ -313,6 +330,7 @@ export const parameters = {
313
330
  query: { type: 'string', required: true, description: 'Keyword to search for' },
314
331
  folder: { type: 'string', description: 'IMAP folder to search in; defaults to the account inboxFolder' },
315
332
  limit: { type: 'integer', description: 'How many matches to return, 1-100, default 10' },
333
+ offset: { type: 'integer', description: 'Skip this many newest matches first, default 0' },
316
334
  since: { type: 'string', description: 'Only search messages received on or after this date, e.g. 2026-08-01 (optional)' },
317
335
  until: { type: 'string', description: 'Only search messages received on or before this date, e.g. 2026-08-26 (optional)' },
318
336
  account: { type: 'string', description: ACCOUNT_HINT },
package/lib/tools.d.ts CHANGED
@@ -9,5 +9,6 @@ export interface EmailToolDefinition {
9
9
  render(args: unknown, value: unknown): TextBlock[];
10
10
  };
11
11
  execute(args: unknown, exec?: unknown): Promise<unknown>;
12
+ timeoutMs?: number;
12
13
  }
13
14
  export declare function buildEmailTools(runtime: Pick<EmailRuntime, 'getPool' | 'getEffectiveSettings' | 'watch'>): EmailToolDefinition[];
package/lib/tools.js CHANGED
@@ -2,6 +2,9 @@ import { clampInt, PROVIDER_PRESETS } from './config.js';
2
2
  import { messageOf } from './mail-client.js';
3
3
  import { NOT_LOGGED_IN_MESSAGE, oauth2StateOf } from './oauth2.js';
4
4
  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';
5
+ /** 单次工具调用的 deadline:普通查询 60s,正文扫描/watch 这类整批操作 120s。 */
6
+ const QUERY_TIMEOUT_MS = 60000;
7
+ const BATCH_TIMEOUT_MS = 120000;
5
8
  export function buildEmailTools(runtime) {
6
9
  const { getPool, getEffectiveSettings, watch: watchCore } = runtime;
7
10
  return [
@@ -13,6 +16,7 @@ export function buildEmailTools(runtime) {
13
16
  schema: listSchema,
14
17
  render: (_args, value) => renderList(value),
15
18
  },
19
+ timeoutMs: QUERY_TIMEOUT_MS,
16
20
  async execute(rawArgs, exec) {
17
21
  const args = rawArgs;
18
22
  const limit = clampInt(args.limit, 20, 1, MAX_LIMIT);
@@ -30,6 +34,7 @@ export function buildEmailTools(runtime) {
30
34
  schema: readSchema,
31
35
  render: (_args, value) => renderRead(value),
32
36
  },
37
+ timeoutMs: QUERY_TIMEOUT_MS,
33
38
  async execute(rawArgs, exec) {
34
39
  const args = rawArgs;
35
40
  if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
@@ -46,6 +51,7 @@ export function buildEmailTools(runtime) {
46
51
  schema: markSchema,
47
52
  render: (_args, value) => renderMark(value),
48
53
  },
54
+ timeoutMs: QUERY_TIMEOUT_MS,
49
55
  async execute(rawArgs, exec) {
50
56
  const args = rawArgs;
51
57
  if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
@@ -69,14 +75,16 @@ export function buildEmailTools(runtime) {
69
75
  schema: listSchema,
70
76
  render: (_args, value) => renderSearch(value),
71
77
  },
78
+ timeoutMs: BATCH_TIMEOUT_MS,
72
79
  async execute(rawArgs, exec) {
73
80
  const args = rawArgs;
74
81
  if (typeof args.query !== 'string' || args.query.trim() === '')
75
82
  throw new Error('query 不能为空');
76
83
  const limit = clampInt(args.limit, 10, 1, MAX_LIMIT);
84
+ const offset = clampInt(args.offset, 0, 0, 10000);
77
85
  const since = args.since?.trim() ? parseEmailDay(args.since, 'since') : undefined;
78
86
  const until = args.until?.trim() ? parseEmailDay(args.until, 'until', true) : undefined;
79
- return await getPool().search(args.account, args.query.trim(), args.folder?.trim() || '', limit, since, until, executionSignal(exec));
87
+ return await getPool().search(args.account, args.query.trim(), args.folder?.trim() || '', limit, offset, since, until, executionSignal(exec));
80
88
  }
81
89
  },
82
90
  {
@@ -87,6 +95,7 @@ export function buildEmailTools(runtime) {
87
95
  schema: sendSchema,
88
96
  render: (_args, value) => renderSend(value),
89
97
  },
98
+ timeoutMs: QUERY_TIMEOUT_MS,
90
99
  async execute(rawArgs, exec) {
91
100
  const args = rawArgs;
92
101
  if (typeof args.to !== 'string' || args.to.trim() === '')
@@ -101,6 +110,7 @@ export function buildEmailTools(runtime) {
101
110
  description: descriptions.email_reply,
102
111
  parameters: parameters.email_reply,
103
112
  output: { schema: replySchema, render: (_args, value) => renderReply(value) },
113
+ timeoutMs: QUERY_TIMEOUT_MS,
104
114
  async execute(rawArgs, exec) {
105
115
  const args = rawArgs;
106
116
  if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
@@ -127,6 +137,7 @@ export function buildEmailTools(runtime) {
127
137
  schema: foldersSchema,
128
138
  render: (_args, value) => renderFolders(value),
129
139
  },
140
+ timeoutMs: QUERY_TIMEOUT_MS,
130
141
  async execute(rawArgs, exec) {
131
142
  const args = rawArgs;
132
143
  return await getPool().folders(args.account, args.subscribedOnly === true, executionSignal(exec));
@@ -137,6 +148,7 @@ export function buildEmailTools(runtime) {
137
148
  description: descriptions.email_health,
138
149
  parameters: parameters.email_health,
139
150
  output: { schema: { type: 'object', additionalProperties: true }, render: renderHealth },
151
+ timeoutMs: QUERY_TIMEOUT_MS,
140
152
  async execute(_rawArgs, exec) {
141
153
  executionSignal(exec)?.throwIfAborted();
142
154
  const checks = [];
@@ -181,6 +193,7 @@ export function buildEmailTools(runtime) {
181
193
  schema: attachmentSchema,
182
194
  render: (_args, value) => renderAttachment(value),
183
195
  },
196
+ timeoutMs: QUERY_TIMEOUT_MS,
184
197
  async execute(rawArgs, exec) {
185
198
  const args = rawArgs;
186
199
  if (typeof args.uid !== 'number' || !Number.isInteger(args.uid) || args.uid <= 0) {
@@ -196,6 +209,7 @@ export function buildEmailTools(runtime) {
196
209
  description: descriptions.email_watch,
197
210
  parameters: parameters.email_watch,
198
211
  output: { schema: watchSchema, render: (_args, value) => renderWatch(value) },
212
+ timeoutMs: BATCH_TIMEOUT_MS,
199
213
  async execute(rawArgs, exec) {
200
214
  const args = rawArgs;
201
215
  const limit = clampInt(args.limit, 20, 1, MAX_LIMIT);
package/lib/types.d.ts CHANGED
@@ -39,6 +39,8 @@ export interface EmailListResult {
39
39
  account: string;
40
40
  count: number;
41
41
  folder: string;
42
+ /** The mailbox's UIDVALIDITY; 0 when the server did not report one. */
43
+ uidValidity: number;
42
44
  messages: ListedMessage[];
43
45
  }
44
46
  export interface EmailReadResult extends ReadMessageBody {
@@ -49,9 +51,16 @@ export interface EmailReadResult extends ReadMessageBody {
49
51
  export interface EmailSearchResult {
50
52
  account: string;
51
53
  query: string;
54
+ /** Server path: the server's match count. Fallback scan: this page's row count (see countKind). */
52
55
  count: number;
53
56
  folder: string;
57
+ /** How many newest matches the caller skipped (0 on the first page). */
58
+ offset: number;
54
59
  messages: ListedMessage[];
60
+ /** Set to 'scanned' when the local body scan produced this page, so count is not a total. */
61
+ countKind?: 'scanned';
62
+ /** How many newest messages the fallback scan looked at (set with countKind='scanned'). */
63
+ scannedLimit?: number;
55
64
  }
56
65
  export interface EmailSendResult {
57
66
  account: string;
@@ -99,6 +108,7 @@ export interface EmailSearchArgs extends AccountArg {
99
108
  query: string;
100
109
  folder?: string;
101
110
  limit?: number;
111
+ offset?: number;
102
112
  since?: string;
103
113
  until?: string;
104
114
  }
@@ -175,6 +185,8 @@ export interface EmailWatchResult {
175
185
  /** Unread messages never reported before (empty on firstRun). */
176
186
  newCount: number;
177
187
  messages: ListedMessage[];
188
+ /** True when a server-side UIDVALIDITY change forced a fresh baseline. */
189
+ reset?: boolean;
178
190
  /** Total unread in the folder right now. */
179
191
  totalUnread: number;
180
192
  }
package/lib/web.d.ts CHANGED
@@ -40,6 +40,12 @@ export interface AccountCardData {
40
40
  * exactly the state that has to be fixed before login can start.
41
41
  */
42
42
  clientId?: string;
43
+ /** Display name for the From header, when the account sets one. */
44
+ senderName?: string;
45
+ /** Login user when it differs from the visible address (`user`). */
46
+ authUser?: string;
47
+ /** Whether a login password separate from `password` is stored. */
48
+ hasAuthPassword?: boolean;
43
49
  /** Login state of an OAuth2 account: none / a device code in flight / logged in. */
44
50
  oauthState: OAuth2State;
45
51
  /** The mailbox address the stored token belongs to (OAuth2 accounts only). */
@@ -77,6 +83,23 @@ export interface AccountCardInput {
77
83
  * 第三方应用注册,所以这是 OAuth2 账号的必填项,而设置面板是用户唯一的常规入口。
78
84
  */
79
85
  clientId?: string;
86
+ /**
87
+ * 发件显示名,三态契约同 clientId:undefined = 本卡片没提供(保留已存的
88
+ * senderName 键),'' = 明确清除,非空 = 写入。只改收件人看到的名称,发件地址
89
+ * 始终是 user。
90
+ */
91
+ senderName?: string;
92
+ /**
93
+ * 登录账号(IMAP/SMTP 认证用),三态契约同上:undefined = 保留,'' = 清除(回到
94
+ * 用 user 登录),非空 = 写入。别名/中继场景下 user 是发件地址,它才是登录名。
95
+ */
96
+ authUser?: string;
97
+ /**
98
+ * 登录账号自己的密码,三态契约与 password 完全相同(undefined = 保留已存的值,
99
+ * '' = 明确清除,非空 = 写入)。只有 authUser 与 user 不同、且密码也不一样时
100
+ * 才需要。
101
+ */
102
+ authPassword?: string;
80
103
  /**
81
104
  * 认证方式覆盖,三态契约同上:undefined = 本卡片没提供(保留已存的 authKind 键),
82
105
  * '' = 明确恢复「自动」(删掉该键,回到按 provider/主机派生),非空 = 钉住。
package/lib/web.js CHANGED
@@ -87,6 +87,11 @@ function buildAccountCards(raw, defaultAccount, presets, tokens = () => ({ state
87
87
  // back for the editor to prefill: an OAuth2 account without one cannot
88
88
  // start a device-code login, and the card is where that gets fixed.
89
89
  const clientId = typeof account.clientId === 'string' ? account.clientId.trim() : '';
90
+ // The display name and the login user are not secrets, so — like clientId —
91
+ // the card hands them back for the editor to prefill. A separate login
92
+ // password is a secret and only ever reported as a boolean.
93
+ const senderName = typeof account.senderName === 'string' ? account.senderName.trim() : '';
94
+ const authUser = typeof account.authUser === 'string' ? account.authUser.trim() : '';
90
95
  const oauth = authKind === 'oauth2' ? tokens(name, user) : { state: 'none' };
91
96
  list.push({
92
97
  name,
@@ -97,6 +102,9 @@ function buildAccountCards(raw, defaultAccount, presets, tokens = () => ({ state
97
102
  authKind,
98
103
  ...(pinned === 'oauth2' || pinned === 'password' ? { authKindDeclared: pinned } : {}),
99
104
  ...(clientId !== '' ? { clientId } : {}),
105
+ ...(senderName !== '' ? { senderName } : {}),
106
+ ...(authUser !== '' ? { authUser } : {}),
107
+ ...(typeof account.authPassword === 'string' && account.authPassword !== '' ? { hasAuthPassword: true } : {}),
100
108
  oauthState: oauth.state,
101
109
  ...(oauth.user !== undefined ? { oauthUser: oauth.user } : {}),
102
110
  imap,
@@ -318,7 +326,7 @@ function persistedProvider(provider, customNames) {
318
326
  * contract on AccountCardInput: the card omits the field whenever the editor has
319
327
  * nothing to say about it, which must leave the stored secret untouched.
320
328
  */
321
- function normalizeCardForYaml(card, customNames, inheritedPassword) {
329
+ function normalizeCardForYaml(card, customNames, inheritedPassword, inheritedAuthPassword) {
322
330
  const out = {};
323
331
  const provider = persistedProvider(card.provider, customNames);
324
332
  if (provider !== undefined)
@@ -355,16 +363,40 @@ function normalizeCardForYaml(card, customNames, inheritedPassword) {
355
363
  }
356
364
  if (card.inboxFolder !== undefined)
357
365
  out.inboxFolder = card.inboxFolder;
366
+ if (card.senderName !== undefined) {
367
+ const senderName = String(card.senderName).trim();
368
+ if (senderName !== '')
369
+ out.senderName = senderName;
370
+ }
371
+ if (card.authUser !== undefined) {
372
+ const authUser = String(card.authUser).trim();
373
+ if (authUser !== '')
374
+ out.authUser = authUser;
375
+ }
376
+ if (card.authPassword === undefined) {
377
+ // Same contract as password: a card that says nothing must not delete it.
378
+ if (inheritedAuthPassword !== undefined)
379
+ out.authPassword = inheritedAuthPassword;
380
+ }
381
+ else if (card.authPassword !== '') {
382
+ out.authPassword = String(card.authPassword);
383
+ }
358
384
  return out;
359
385
  }
360
- /** The `password` stored in the source YAML for one account, if the key is there. */
361
- function storedPasswordOf(raw, name) {
386
+ /** One secret key stored in the source YAML for one account, if it is there. */
387
+ function storedSecretOf(raw, name, key) {
362
388
  const account = raw[name];
363
389
  if (account === null || typeof account !== 'object' || Array.isArray(account))
364
390
  return { present: false, value: undefined };
365
- if (!Object.prototype.hasOwnProperty.call(account, 'password'))
391
+ if (!Object.prototype.hasOwnProperty.call(account, key))
366
392
  return { present: false, value: undefined };
367
- return { present: true, value: account.password };
393
+ return { present: true, value: account[key] };
394
+ }
395
+ function storedPasswordOf(raw, name) {
396
+ return storedSecretOf(raw, name, 'password');
397
+ }
398
+ function storedAuthPasswordOf(raw, name) {
399
+ return storedSecretOf(raw, name, 'authPassword');
368
400
  }
369
401
  /**
370
402
  * Fallback writer: loses comments but keeps the semantics the cards describe.
@@ -390,6 +422,7 @@ function fallbackSerialize(cards, defaultAccount, source, customNames) {
390
422
  for (const card of cards) {
391
423
  const name = card.name;
392
424
  let inherited;
425
+ let inheritedAuthPassword;
393
426
  if (stored === undefined) {
394
427
  // Nothing could be read back: a silent card may be losing a real secret.
395
428
  if (card.password === undefined)
@@ -399,8 +432,11 @@ function fallbackSerialize(cards, defaultAccount, source, customNames) {
399
432
  const { present, value } = storedPasswordOf(stored, name);
400
433
  if (present)
401
434
  inherited = value;
435
+ const auth = storedAuthPasswordOf(stored, name);
436
+ if (auth.present)
437
+ inheritedAuthPassword = auth.value;
402
438
  }
403
- raw[name] = normalizeCardForYaml(card, customNames, inherited);
439
+ raw[name] = normalizeCardForYaml(card, customNames, inherited, inheritedAuthPassword);
404
440
  }
405
441
  return {
406
442
  accountsYaml: serializeAccountsYaml(raw, defaultAccount),
@@ -485,6 +521,26 @@ function serializeAccountsDraft(source, cards, defaultAccount, customNames) {
485
521
  const nextProvider = persistedProvider(card.provider, customNames);
486
522
  writeField(account, 'provider', nextProvider);
487
523
  writeField(account, 'user', card.user);
524
+ // senderName / authUser are plain three-state fields (undefined = 保持原样,
525
+ // '' = 清除, 非空 = 写入) — writeField already implements exactly that.
526
+ // An undefined field means "this card says nothing" — writeField would read
527
+ // that as「delete」, so the guard has to live here, not inside it.
528
+ if (card.senderName !== undefined)
529
+ writeField(account, 'senderName', String(card.senderName).trim());
530
+ if (card.authUser !== undefined)
531
+ writeField(account, 'authUser', String(card.authUser).trim());
532
+ // authPassword is a secret and follows the password contract verbatim: the
533
+ // card never carries the plaintext, so an omitted field must leave the
534
+ // stored key — value, position and comment — untouched.
535
+ if (card.authPassword === undefined) {
536
+ // 未提供 = 保持原样:什么都不写。
537
+ }
538
+ else if (card.authPassword === '') {
539
+ account.delete('authPassword');
540
+ }
541
+ else {
542
+ account.set('authPassword', String(card.authPassword));
543
+ }
488
544
  // Password is three-state, unlike every other field: the card is never given
489
545
  // the plaintext (snapshot exposes hasPassword only), so an omitted password
490
546
  // means "the editor has nothing to say" and the stored key must survive
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-email",
3
- "version": "0.11.0",
4
- "description": "DeepSeek Harness 邮件插件:IMAP/SMTP 收发、搜索、回复转发、邮件整理与增量收件,支持多邮箱预设、发信审批及 Web 设置与新邮件通知。",
3
+ "version": "0.13.0",
4
+ "description": "DSH 邮件插件:IMAP/SMTP 收发搜索、回复转发、附件与整理,多账号卡片设置页 + Outlook OAuth2 + 发信审批。",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "types": "lib/index.d.ts",
@@ -19,7 +19,8 @@
19
19
  "assets/whale-fallback.png",
20
20
  "cordis.patch.yml",
21
21
  "README.md",
22
- "README.en.md"
22
+ "README.en.md",
23
+ "CHANGELOG.md"
23
24
  ],
24
25
  "scripts": {
25
26
  "build": "tsc",