@zhin.js/adapter-email 4.0.3 → 5.0.1
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/CHANGELOG.md +33 -0
- package/adapters/email.js +5 -3
- package/adapters/email.ts +5 -3
- package/lib/protocol.d.ts +13 -4
- package/lib/protocol.js +52 -10
- package/package.json +11 -11
- package/src/protocol.ts +68 -14
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# @zhin.js/adapter-email
|
|
2
2
|
|
|
3
|
+
## 5.0.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- Updated dependencies [c8f4d45]
|
|
8
|
+
- @zhin.js/plugin-runtime@1.1.2
|
|
9
|
+
- @zhin.js/adapter@1.1.4
|
|
10
|
+
- @zhin.js/core@1.5.1
|
|
11
|
+
- zhin.js@6.0.1
|
|
12
|
+
|
|
13
|
+
## 5.0.0
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 4fbff5d: feat!: 多模态双向 Segment 一贯制(BREAKING,无兼容层)
|
|
18
|
+
|
|
19
|
+
全框架唯一媒体表达统一为 canonical `Segment` + `MediaRef{kind: url|path|base64|file, value, mime_type?, file_name?, size?}`,新增 audio/video/file 段类型;所有第二形状(legacy `data.url/file/base64` 字段、`mediaRefFromLegacyData`/`mediaRefToLegacyFields` 桥、双写)全部删除。
|
|
20
|
+
|
|
21
|
+
- **core**:`SendContent` 一等支持 `Segment[]`;endpoint 出站载荷只含 canonical 段;`resolveOutboundMediaPolicy` 改为纯声明驱动(adapter definition `segments.outboundMedia`),内置策略表删除,未声明回退 `url-or-text`;`ImageContent` 旧桥删除。
|
|
22
|
+
- **ai**:新增 `MediaContentBlock`/`MediaBlockRef`(Segment 同构)与 `UserMessage.media`(当前 turn 媒体,**不持久化**——存储层自动剥离);`createUserMessage(text, media?)` 签名变更(`ImageContent` 删除);provider 边界序列化器 `filterMediaBlocksForProvider` + 能力表(缺省 image-only,不支持类型降级占位文本);ai-sdk 桥媒体块 → SDK image/file parts。
|
|
23
|
+
- **agent**:入站 turn 注入(`turn/inbound-media.ts`)——commMessage 媒体段 → 当前 turn `UserMessage.media`;图片 path 物化、音频默认 STT(`@zhin.js/speech` 可选,失败降级占位)、视频/文件占位;`publishOutboundElements` 产出 canonical Segment;`transcribeAudioPayload` 导出。
|
|
24
|
+
- **cli**:`bridgeRuntimeMessage` 回复链路媒体段透传,不再压平为文本(`$reply` 直达 normalize → adapter)。
|
|
25
|
+
- **全部 20 个平台适配器**:出站媒体只消费 `data.media`(url 直发 / base64 直发 / 平台上传 / 读盘),入站媒体产出 canonical `data.media`;`segments.outboundMedia` 声明与实际消费逐一核对修正;QQ 入站新增 canonical segments(image/audio/video/file/mention/face/reply),图片/语音/视频不再丢失。
|
|
26
|
+
|
|
27
|
+
迁移:适配器/插件产媒体一律用 `{ type, data: { media: MediaRef } }`;发送 legacy `data.url/file/base64` 形状的段会被 warn 丢弃。
|
|
28
|
+
|
|
29
|
+
- Updated dependencies [7c1e63a]
|
|
30
|
+
- Updated dependencies [4fbff5d]
|
|
31
|
+
- Updated dependencies [5b94d9c]
|
|
32
|
+
- @zhin.js/adapter@1.1.3
|
|
33
|
+
- @zhin.js/core@1.5.0
|
|
34
|
+
- zhin.js@6.0.0
|
|
35
|
+
|
|
3
36
|
## 4.0.3
|
|
4
37
|
|
|
5
38
|
### Patch Changes
|
package/adapters/email.js
CHANGED
|
@@ -9,10 +9,12 @@ import { resolveEmailConfig, } from "../lib/protocol.js";
|
|
|
9
9
|
export { EmailEndpoint } from "../lib/endpoint.js";
|
|
10
10
|
export default defineAdapter({
|
|
11
11
|
capabilities: ['inbound', 'outbound'],
|
|
12
|
-
// image/file 段映射为邮件附件:canonical MediaRef kind=url/path
|
|
13
|
-
// nodemailer attachment.path(URL 由 nodemailer
|
|
12
|
+
// image/audio/video/file 段映射为邮件附件:canonical MediaRef kind=url/path
|
|
13
|
+
// 作 nodemailer attachment.path(URL 由 nodemailer 拉流、path 读盘),
|
|
14
|
+
// kind=base64 直发(content + encoding);kind=file 无邮件对应概念,丢弃留痕。
|
|
15
|
+
// 邮件无交互面,交互段降级纯文本。
|
|
14
16
|
segments: {
|
|
15
|
-
outboundMedia: ['url', 'path'],
|
|
17
|
+
outboundMedia: ['url', 'path', 'base64'],
|
|
16
18
|
interactive: 'text',
|
|
17
19
|
},
|
|
18
20
|
create(context) {
|
package/adapters/email.ts
CHANGED
|
@@ -19,10 +19,12 @@ export type {
|
|
|
19
19
|
|
|
20
20
|
export default defineAdapter<EmailAdapterConfig>({
|
|
21
21
|
capabilities: ['inbound', 'outbound'],
|
|
22
|
-
// image/file 段映射为邮件附件:canonical MediaRef kind=url/path
|
|
23
|
-
// nodemailer attachment.path(URL 由 nodemailer
|
|
22
|
+
// image/audio/video/file 段映射为邮件附件:canonical MediaRef kind=url/path
|
|
23
|
+
// 作 nodemailer attachment.path(URL 由 nodemailer 拉流、path 读盘),
|
|
24
|
+
// kind=base64 直发(content + encoding);kind=file 无邮件对应概念,丢弃留痕。
|
|
25
|
+
// 邮件无交互面,交互段降级纯文本。
|
|
24
26
|
segments: {
|
|
25
|
-
outboundMedia: ['url', 'path'],
|
|
27
|
+
outboundMedia: ['url', 'path', 'base64'],
|
|
26
28
|
interactive: 'text',
|
|
27
29
|
},
|
|
28
30
|
create(context) {
|
package/lib/protocol.d.ts
CHANGED
|
@@ -102,6 +102,18 @@ export interface SavedEmailAttachment {
|
|
|
102
102
|
*/
|
|
103
103
|
export declare function formatInboundSegments(email: EmailMessage, savedAttachments?: readonly SavedEmailAttachment[]): Segment[];
|
|
104
104
|
export declare function senderDisplayName(from: string): string;
|
|
105
|
+
/**
|
|
106
|
+
* nodemailer 附件的最小形状:url/path 走 `path`(URL 由 nodemailer 拉流、
|
|
107
|
+
* 本地路径读盘),base64 走 `content` + `encoding: 'base64'` 直发。
|
|
108
|
+
*/
|
|
109
|
+
export type EmailOutboundAttachment = {
|
|
110
|
+
filename: string;
|
|
111
|
+
path: string;
|
|
112
|
+
} | {
|
|
113
|
+
filename: string;
|
|
114
|
+
content: string;
|
|
115
|
+
encoding: 'base64';
|
|
116
|
+
};
|
|
105
117
|
/**
|
|
106
118
|
* Wire-encode an already-rendered outbound payload into nodemailer options.
|
|
107
119
|
* Segment canonicalization is intentionally not done here.
|
|
@@ -116,8 +128,5 @@ export declare function formatOutboundMail(payload: unknown, options: {
|
|
|
116
128
|
subject: string;
|
|
117
129
|
text?: string;
|
|
118
130
|
html?: string;
|
|
119
|
-
attachments?:
|
|
120
|
-
filename: string;
|
|
121
|
-
path: string;
|
|
122
|
-
}>;
|
|
131
|
+
attachments?: EmailOutboundAttachment[];
|
|
123
132
|
};
|
package/lib/protocol.js
CHANGED
|
@@ -2,7 +2,9 @@
|
|
|
2
2
|
* Email SMTP/IMAP helpers (no legacy Adapter/Endpoint / segment-mapper).
|
|
3
3
|
* Canonicalization is owned by gateway/core before endpoint.send.
|
|
4
4
|
*/
|
|
5
|
-
import { htmlToPlainTextWithBlockBreaks, isMediaRef
|
|
5
|
+
import { htmlToPlainTextWithBlockBreaks, isMediaRef } from '@zhin.js/core';
|
|
6
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
7
|
+
const logger = getLogger('email');
|
|
6
8
|
export function resolveEmailConfig(config = {}) {
|
|
7
9
|
const entry = config.endpoints?.find((item) => item.context === 'email');
|
|
8
10
|
const smtp = config.smtp ?? entry?.smtp;
|
|
@@ -122,6 +124,50 @@ export function senderDisplayName(from) {
|
|
|
122
124
|
const name = from.split('<')[0]?.trim();
|
|
123
125
|
return name || from;
|
|
124
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* image/audio/video/file 段 → nodemailer 附件(canonical MediaRef 唯一来源):
|
|
129
|
+
* - kind=url / path → attachment.path;
|
|
130
|
+
* - kind=base64 → attachment.content(data: URL 前缀剥离);
|
|
131
|
+
* - kind=file(平台不透明引用)邮件无对应概念,丢弃留痕;
|
|
132
|
+
* - 缺 media 同样 warn + 丢弃。
|
|
133
|
+
*/
|
|
134
|
+
function mediaSegmentToAttachment(type, data) {
|
|
135
|
+
const media = data.media;
|
|
136
|
+
if (!isMediaRef(media)) {
|
|
137
|
+
logger.warn(formatCompact({
|
|
138
|
+
op: 'email_outbound_media_dropped',
|
|
139
|
+
type,
|
|
140
|
+
reason: 'missing_media_ref',
|
|
141
|
+
}));
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
if (media.kind === 'file') {
|
|
145
|
+
logger.warn(formatCompact({
|
|
146
|
+
op: 'email_outbound_media_dropped',
|
|
147
|
+
type,
|
|
148
|
+
reason: 'unsupported_kind',
|
|
149
|
+
kind: media.kind,
|
|
150
|
+
}));
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
const filename = attachmentFileName(type, data, media);
|
|
154
|
+
if (media.kind === 'base64') {
|
|
155
|
+
const value = media.value.startsWith('data:')
|
|
156
|
+
? media.value.slice(media.value.indexOf(',') + 1)
|
|
157
|
+
: media.value;
|
|
158
|
+
return { filename, content: value, encoding: 'base64' };
|
|
159
|
+
}
|
|
160
|
+
return { filename, path: media.value };
|
|
161
|
+
}
|
|
162
|
+
function attachmentFileName(type, data, media) {
|
|
163
|
+
if (media.file_name)
|
|
164
|
+
return media.file_name;
|
|
165
|
+
if (typeof data.name === 'string' && data.name)
|
|
166
|
+
return data.name;
|
|
167
|
+
if (typeof data.alt === 'string' && data.alt)
|
|
168
|
+
return data.alt;
|
|
169
|
+
return type === 'image' ? 'image.png' : 'file';
|
|
170
|
+
}
|
|
125
171
|
/**
|
|
126
172
|
* Wire-encode an already-rendered outbound payload into nodemailer options.
|
|
127
173
|
* Segment canonicalization is intentionally not done here.
|
|
@@ -166,16 +212,12 @@ export function formatOutboundMail(payload, options) {
|
|
|
166
212
|
break;
|
|
167
213
|
}
|
|
168
214
|
case 'image':
|
|
215
|
+
case 'audio':
|
|
216
|
+
case 'video':
|
|
169
217
|
case 'file': {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
if (media && (media.kind === 'url' || media.kind === 'path')) {
|
|
174
|
-
attachments.push({
|
|
175
|
-
filename: String(data.filename || data.name || (item.type === 'image' ? 'image.png' : 'file')),
|
|
176
|
-
path: media.value,
|
|
177
|
-
});
|
|
178
|
-
}
|
|
218
|
+
const attachment = mediaSegmentToAttachment(item.type, data);
|
|
219
|
+
if (attachment)
|
|
220
|
+
attachments.push(attachment);
|
|
179
221
|
break;
|
|
180
222
|
}
|
|
181
223
|
default:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zhin.js/adapter-email",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Zhin.js Email adapter for Plugin Runtime (SMTP/IMAP)",
|
|
6
6
|
"main": "./lib/index.js",
|
|
@@ -9,16 +9,16 @@
|
|
|
9
9
|
"imap": "^0.8.19",
|
|
10
10
|
"mailparser": "^3.9.14",
|
|
11
11
|
"nodemailer": "^9.0.3",
|
|
12
|
-
"@zhin.js/adapter": "1.1.
|
|
13
|
-
"@zhin.js/core": "1.
|
|
12
|
+
"@zhin.js/adapter": "1.1.4",
|
|
13
|
+
"@zhin.js/core": "1.5.1",
|
|
14
14
|
"@zhin.js/logger": "1.0.75",
|
|
15
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
15
|
+
"@zhin.js/plugin-runtime": "1.1.2"
|
|
16
16
|
},
|
|
17
17
|
"peerDependencies": {
|
|
18
|
-
"@zhin.js/adapter": "1.1.
|
|
19
|
-
"@zhin.js/core": "1.
|
|
20
|
-
"@zhin.js/plugin-runtime": "1.1.
|
|
21
|
-
"zhin.js": "
|
|
18
|
+
"@zhin.js/adapter": "1.1.4",
|
|
19
|
+
"@zhin.js/core": "1.5.1",
|
|
20
|
+
"@zhin.js/plugin-runtime": "1.1.2",
|
|
21
|
+
"zhin.js": "6.0.1"
|
|
22
22
|
},
|
|
23
23
|
"peerDependenciesMeta": {
|
|
24
24
|
"zhin.js": {
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
}
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@types/imap": "^0.8.
|
|
29
|
+
"@types/imap": "^0.8.43",
|
|
30
30
|
"@types/mailparser": "^3.4.6",
|
|
31
|
-
"@types/node": "^26.1.
|
|
32
|
-
"@types/nodemailer": "^8.0.
|
|
31
|
+
"@types/node": "^26.1.2",
|
|
32
|
+
"@types/nodemailer": "^8.0.1",
|
|
33
33
|
"typescript": "^6.0.3",
|
|
34
34
|
"vitest": "^4.1.10"
|
|
35
35
|
},
|
package/src/protocol.ts
CHANGED
|
@@ -4,8 +4,11 @@
|
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import type { Attachment } from 'mailparser';
|
|
7
|
-
import { htmlToPlainTextWithBlockBreaks, isMediaRef,
|
|
7
|
+
import { htmlToPlainTextWithBlockBreaks, isMediaRef, type MediaRef } from '@zhin.js/core';
|
|
8
8
|
import type { Segment } from '@zhin.js/core/runtime';
|
|
9
|
+
import { formatCompact, getLogger } from '@zhin.js/logger';
|
|
10
|
+
|
|
11
|
+
const logger = getLogger('email');
|
|
9
12
|
|
|
10
13
|
export interface SmtpConfig {
|
|
11
14
|
readonly host: string;
|
|
@@ -229,6 +232,64 @@ export function senderDisplayName(from: string): string {
|
|
|
229
232
|
return name || from;
|
|
230
233
|
}
|
|
231
234
|
|
|
235
|
+
/**
|
|
236
|
+
* nodemailer 附件的最小形状:url/path 走 `path`(URL 由 nodemailer 拉流、
|
|
237
|
+
* 本地路径读盘),base64 走 `content` + `encoding: 'base64'` 直发。
|
|
238
|
+
*/
|
|
239
|
+
export type EmailOutboundAttachment =
|
|
240
|
+
| { filename: string; path: string }
|
|
241
|
+
| { filename: string; content: string; encoding: 'base64' };
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* image/audio/video/file 段 → nodemailer 附件(canonical MediaRef 唯一来源):
|
|
245
|
+
* - kind=url / path → attachment.path;
|
|
246
|
+
* - kind=base64 → attachment.content(data: URL 前缀剥离);
|
|
247
|
+
* - kind=file(平台不透明引用)邮件无对应概念,丢弃留痕;
|
|
248
|
+
* - 缺 media 同样 warn + 丢弃。
|
|
249
|
+
*/
|
|
250
|
+
function mediaSegmentToAttachment(
|
|
251
|
+
type: string,
|
|
252
|
+
data: Record<string, unknown>,
|
|
253
|
+
): EmailOutboundAttachment | null {
|
|
254
|
+
const media = data.media;
|
|
255
|
+
if (!isMediaRef(media)) {
|
|
256
|
+
logger.warn(formatCompact({
|
|
257
|
+
op: 'email_outbound_media_dropped',
|
|
258
|
+
type,
|
|
259
|
+
reason: 'missing_media_ref',
|
|
260
|
+
}));
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
if (media.kind === 'file') {
|
|
264
|
+
logger.warn(formatCompact({
|
|
265
|
+
op: 'email_outbound_media_dropped',
|
|
266
|
+
type,
|
|
267
|
+
reason: 'unsupported_kind',
|
|
268
|
+
kind: media.kind,
|
|
269
|
+
}));
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
const filename = attachmentFileName(type, data, media);
|
|
273
|
+
if (media.kind === 'base64') {
|
|
274
|
+
const value = media.value.startsWith('data:')
|
|
275
|
+
? media.value.slice(media.value.indexOf(',') + 1)
|
|
276
|
+
: media.value;
|
|
277
|
+
return { filename, content: value, encoding: 'base64' };
|
|
278
|
+
}
|
|
279
|
+
return { filename, path: media.value };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function attachmentFileName(
|
|
283
|
+
type: string,
|
|
284
|
+
data: Record<string, unknown>,
|
|
285
|
+
media: MediaRef,
|
|
286
|
+
): string {
|
|
287
|
+
if (media.file_name) return media.file_name;
|
|
288
|
+
if (typeof data.name === 'string' && data.name) return data.name;
|
|
289
|
+
if (typeof data.alt === 'string' && data.alt) return data.alt;
|
|
290
|
+
return type === 'image' ? 'image.png' : 'file';
|
|
291
|
+
}
|
|
292
|
+
|
|
232
293
|
/**
|
|
233
294
|
* Wire-encode an already-rendered outbound payload into nodemailer options.
|
|
234
295
|
* Segment canonicalization is intentionally not done here.
|
|
@@ -242,7 +303,7 @@ export function formatOutboundMail(
|
|
|
242
303
|
subject: string;
|
|
243
304
|
text?: string;
|
|
244
305
|
html?: string;
|
|
245
|
-
attachments?:
|
|
306
|
+
attachments?: EmailOutboundAttachment[];
|
|
246
307
|
} {
|
|
247
308
|
const mail = {
|
|
248
309
|
from: options.from,
|
|
@@ -271,7 +332,7 @@ export function formatOutboundMail(
|
|
|
271
332
|
|
|
272
333
|
const textParts: string[] = [];
|
|
273
334
|
const htmlParts: string[] = [];
|
|
274
|
-
const attachments:
|
|
335
|
+
const attachments: EmailOutboundAttachment[] = [];
|
|
275
336
|
|
|
276
337
|
for (const item of segments) {
|
|
277
338
|
if (typeof item === 'string') {
|
|
@@ -288,18 +349,11 @@ export function formatOutboundMail(
|
|
|
288
349
|
break;
|
|
289
350
|
}
|
|
290
351
|
case 'image':
|
|
352
|
+
case 'audio':
|
|
353
|
+
case 'video':
|
|
291
354
|
case 'file': {
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
const media = isMediaRef(data.media) ? data.media : mediaRefFromLegacyData(data);
|
|
295
|
-
if (media && (media.kind === 'url' || media.kind === 'path')) {
|
|
296
|
-
attachments.push({
|
|
297
|
-
filename: String(
|
|
298
|
-
data.filename || data.name || (item.type === 'image' ? 'image.png' : 'file'),
|
|
299
|
-
),
|
|
300
|
-
path: media.value,
|
|
301
|
-
});
|
|
302
|
-
}
|
|
355
|
+
const attachment = mediaSegmentToAttachment(item.type, data);
|
|
356
|
+
if (attachment) attachments.push(attachment);
|
|
303
357
|
break;
|
|
304
358
|
}
|
|
305
359
|
default:
|