@zhin.js/adapter-wecom 3.0.6 → 4.0.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # @zhin.js/adapter-wecom
2
2
 
3
+ ## 4.0.0
4
+
5
+ ### Patch Changes
6
+
7
+ - 4fbff5d: feat!: 多模态双向 Segment 一贯制(BREAKING,无兼容层)
8
+
9
+ 全框架唯一媒体表达统一为 canonical `Segment` + `MediaRef{kind: url|path|base64|file, value, mime_type?, file_name?, size?}`,新增 audio/video/file 段类型;所有第二形状(legacy `data.url/file/base64` 字段、`mediaRefFromLegacyData`/`mediaRefToLegacyFields` 桥、双写)全部删除。
10
+
11
+ - **core**:`SendContent` 一等支持 `Segment[]`;endpoint 出站载荷只含 canonical 段;`resolveOutboundMediaPolicy` 改为纯声明驱动(adapter definition `segments.outboundMedia`),内置策略表删除,未声明回退 `url-or-text`;`ImageContent` 旧桥删除。
12
+ - **ai**:新增 `MediaContentBlock`/`MediaBlockRef`(Segment 同构)与 `UserMessage.media`(当前 turn 媒体,**不持久化**——存储层自动剥离);`createUserMessage(text, media?)` 签名变更(`ImageContent` 删除);provider 边界序列化器 `filterMediaBlocksForProvider` + 能力表(缺省 image-only,不支持类型降级占位文本);ai-sdk 桥媒体块 → SDK image/file parts。
13
+ - **agent**:入站 turn 注入(`turn/inbound-media.ts`)——commMessage 媒体段 → 当前 turn `UserMessage.media`;图片 path 物化、音频默认 STT(`@zhin.js/speech` 可选,失败降级占位)、视频/文件占位;`publishOutboundElements` 产出 canonical Segment;`transcribeAudioPayload` 导出。
14
+ - **cli**:`bridgeRuntimeMessage` 回复链路媒体段透传,不再压平为文本(`$reply` 直达 normalize → adapter)。
15
+ - **全部 20 个平台适配器**:出站媒体只消费 `data.media`(url 直发 / base64 直发 / 平台上传 / 读盘),入站媒体产出 canonical `data.media`;`segments.outboundMedia` 声明与实际消费逐一核对修正;QQ 入站新增 canonical segments(image/audio/video/file/mention/face/reply),图片/语音/视频不再丢失。
16
+
17
+ 迁移:适配器/插件产媒体一律用 `{ type, data: { media: MediaRef } }`;发送 legacy `data.url/file/base64` 形状的段会被 warn 丢弃。
18
+
19
+ - Updated dependencies [7c1e63a]
20
+ - Updated dependencies [4fbff5d]
21
+ - Updated dependencies [5b94d9c]
22
+ - @zhin.js/command@1.0.5
23
+ - @zhin.js/adapter@1.1.3
24
+ - @zhin.js/core@1.5.0
25
+ - @zhin.js/agent@1.1.0
26
+ - zhin.js@6.0.0
27
+
3
28
  ## 3.0.6
4
29
 
5
30
  ### Patch Changes
package/adapters/wecom.js CHANGED
@@ -12,15 +12,15 @@ import { wecomRuntimeStateToken } from "../lib/wecom-runtime-state.js";
12
12
  export { WecomEndpoint } from "../lib/endpoint.js";
13
13
  export default defineAdapter({
14
14
  capabilities: ['inbound', 'outbound'],
15
- // image 段经 /cgi-bin/media/upload 物化为 media_id(url 下载后上传);
16
- // 无卡片交互面,交互段降级纯文本。
15
+ // image 段全部经 /cgi-bin/media/upload 物化为 media_id(url 下载后上传、
16
+ // base64/path 直接上传、file 引用直用 media_id);无卡片交互面,交互段降级纯文本。
17
17
  segments: {
18
- outboundMedia: ['url', 'upload'],
18
+ outboundMedia: ['upload'],
19
19
  interactive: 'text',
20
20
  },
21
21
  create(context) {
22
22
  const config = resolveWecomConfig(context.config);
23
- // 注册到插件运行时状态(wecom endpoint list 的"运行中"数据源)
23
+ // 注册到插件运行时状态(wecom.endpoint list 的"运行中"数据源)
24
24
  context.use(wecomRuntimeStateToken).endpoints.set(config.name, {
25
25
  name: config.name,
26
26
  mode: 'webhook',
package/adapters/wecom.ts CHANGED
@@ -17,15 +17,15 @@ export type { WecomEndpointOptions, WecomFetch } from '../src/endpoint.js';
17
17
 
18
18
  export default defineAdapter<WecomAdapterConfig>({
19
19
  capabilities: ['inbound', 'outbound'],
20
- // image 段经 /cgi-bin/media/upload 物化为 media_id(url 下载后上传);
21
- // 无卡片交互面,交互段降级纯文本。
20
+ // image 段全部经 /cgi-bin/media/upload 物化为 media_id(url 下载后上传、
21
+ // base64/path 直接上传、file 引用直用 media_id);无卡片交互面,交互段降级纯文本。
22
22
  segments: {
23
- outboundMedia: ['url', 'upload'],
23
+ outboundMedia: ['upload'],
24
24
  interactive: 'text',
25
25
  },
26
26
  create(context) {
27
27
  const config = resolveWecomConfig(context.config);
28
- // 注册到插件运行时状态(wecom endpoint list 的"运行中"数据源)
28
+ // 注册到插件运行时状态(wecom.endpoint list 的"运行中"数据源)
29
29
  context.use(wecomRuntimeStateToken).endpoints.set(config.name, {
30
30
  name: config.name,
31
31
  mode: 'webhook',
package/lib/endpoint.js CHANGED
@@ -89,13 +89,15 @@ export class WecomEndpoint {
89
89
  });
90
90
  }
91
91
  /**
92
- * message/send 的 image 段只接受 media_id:canonical MediaRef(base64/本地路径/URL)
93
- * 先经 /cgi-bin/media/upload 物化;上传失败降级为文本(alt 优先),不阻断发送。
92
+ * message/send 的 image 段只接受 media_id:canonical MediaRef 按 kind 投递——
93
+ * file(平台不透明引用)直用为 media_id;base64/本地路径/URL 先经
94
+ * /cgi-bin/media/upload 物化;上传失败降级为文本(alt 优先),不阻断发送。
95
+ * 无 canonical MediaRef 的媒体段 warn + 丢弃。
94
96
  */
95
97
  async #materializeOutboundMedia(payload) {
96
98
  if (!Array.isArray(payload))
97
99
  return payload;
98
- return Promise.all(payload.map(async (item) => {
100
+ const items = await Promise.all(payload.map(async (item) => {
99
101
  if (typeof item === 'string' || !item || typeof item !== 'object')
100
102
  return item;
101
103
  const seg = item;
@@ -103,8 +105,18 @@ export class WecomEndpoint {
103
105
  return item;
104
106
  const data = seg.data ?? {};
105
107
  const media = readOutboundImageMedia(data);
106
- if (!media)
107
- return item;
108
+ if (!media) {
109
+ logger.warn(formatCompact({
110
+ op: 'wecom_outbound_media_dropped',
111
+ endpoint: this.#options.config.name,
112
+ type: 'image',
113
+ reason: 'missing_media_ref',
114
+ }));
115
+ return null;
116
+ }
117
+ if (media.kind === 'file') {
118
+ return { type: 'image', data: { media_id: media.value } };
119
+ }
108
120
  try {
109
121
  const mediaId = await this.#uploadMedia('image', media);
110
122
  return { type: 'image', data: { media_id: mediaId } };
@@ -119,6 +131,7 @@ export class WecomEndpoint {
119
131
  return { type: 'text', data: { text: alt } };
120
132
  }
121
133
  }));
134
+ return items.filter((item) => item !== null);
122
135
  }
123
136
  /** POST /cgi-bin/media/upload(临时素材,3 天有效),返回 media_id。 */
124
137
  async #uploadMedia(type, media) {
@@ -16,7 +16,8 @@ export declare function resolveMediaBinary(media: MediaRef, download?: (url: str
16
16
  /** 临时素材上传的 multipart body(字段名固定为 `media`)。 */
17
17
  export declare function buildMediaUploadForm(binary: MediaBinary): FormData;
18
18
  /**
19
- * 出站 image 段的媒体引用:已有 media_id 的视为已物化;
20
- * 否则读 canonical `data.media`,兼容旧 wire `{url,file,base64}` 字段。
19
+ * 出站 image 段的媒体引用:只读 canonical `data.media`(MediaRef)。
20
+ * 中央 normalizeOutboundPayload 已保证到达 endpoint 的载荷为 canonical;
21
+ * 无 MediaRef 时返回 undefined,由调用方 warn + 丢弃。
21
22
  */
22
23
  export declare function readOutboundImageMedia(data: Record<string, unknown>): MediaRef | undefined;
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { readFile } from 'node:fs/promises';
7
7
  import { basename } from 'node:path';
8
- import { isMediaRef, mediaRefFromLegacyData } from '@zhin.js/core';
8
+ import { isMediaRef } from '@zhin.js/core';
9
9
  const MIME_EXT = {
10
10
  'image/jpeg': 'jpg',
11
11
  'image/png': 'png',
@@ -27,7 +27,11 @@ export async function resolveMediaBinary(media, download = defaultDownload) {
27
27
  const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
28
28
  return { data: await readFile(path), mimeType, fileName: basename(path) };
29
29
  }
30
- return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
30
+ if (media.kind === 'url') {
31
+ return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
32
+ }
33
+ // kind=file 为平台不透明引用(media_id),无二进制可解,调用方应直用 value
34
+ throw new Error(`cannot resolve binary from media kind: ${media.kind}`);
31
35
  }
32
36
  async function defaultDownload(url) {
33
37
  const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
@@ -45,15 +49,10 @@ export function buildMediaUploadForm(binary) {
45
49
  return form;
46
50
  }
47
51
  /**
48
- * 出站 image 段的媒体引用:已有 media_id 的视为已物化;
49
- * 否则读 canonical `data.media`,兼容旧 wire `{url,file,base64}` 字段。
52
+ * 出站 image 段的媒体引用:只读 canonical `data.media`(MediaRef)。
53
+ * 中央 normalizeOutboundPayload 已保证到达 endpoint 的载荷为 canonical;
54
+ * 无 MediaRef 时返回 undefined,由调用方 warn + 丢弃。
50
55
  */
51
56
  export function readOutboundImageMedia(data) {
52
- if (typeof data.media_id === 'string' && data.media_id)
53
- return undefined;
54
- if (typeof data.mediaId === 'string' && data.mediaId)
55
- return undefined;
56
- if (isMediaRef(data.media))
57
- return data.media;
58
- return mediaRefFromLegacyData(data);
57
+ return isMediaRef(data.media) ? data.media : undefined;
59
58
  }
package/lib/protocol.js CHANGED
@@ -192,8 +192,8 @@ export function formatOutboundBody(payload) {
192
192
  if (!hasMedia) {
193
193
  hasMedia = true;
194
194
  mediaType = 'image';
195
- // media_id 由 endpoint 上传物化写入;file/url 为旧调用方直传 media_id 的兼容入口。
196
- mediaData = { media_id: data.media_id ?? data.mediaId ?? data.file ?? data.url };
195
+ // media_id 由 endpoint #materializeOutboundMedia 物化写入(内部 wire)。
196
+ mediaData = { media_id: data.media_id };
197
197
  }
198
198
  break;
199
199
  case 'markdown':
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `wecom endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `wecom.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
3
  * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
4
  */
5
5
  import { createEndpointCommands } from '@zhin.js/adapter';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * WeCom 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `wecom endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `wecom.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6
  export const wecomRuntimeStateToken = defineEndpointRuntimeStateToken('wecom');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-wecom",
3
- "version": "3.0.6",
3
+ "version": "4.0.0",
4
4
  "description": "Zhin.js WeCom (企业微信) adapter for Plugin Runtime (HTTP webhook)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,21 +34,21 @@
34
34
  "directory": "plugins/adapters/wecom"
35
35
  },
36
36
  "dependencies": {
37
- "@zhin.js/command": "1.0.4",
38
- "@zhin.js/adapter": "1.1.2",
39
- "@zhin.js/core": "1.4.3",
37
+ "@zhin.js/command": "1.0.5",
38
+ "@zhin.js/adapter": "1.1.3",
39
+ "@zhin.js/core": "1.5.0",
40
40
  "@zhin.js/host-http": "1.0.4",
41
41
  "@zhin.js/logger": "1.0.75",
42
42
  "@zhin.js/plugin-runtime": "1.1.1"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "zod": "^4.0.0",
46
- "@zhin.js/adapter": "1.1.2",
47
- "@zhin.js/agent": "1.0.10",
48
- "@zhin.js/core": "1.4.3",
49
- "@zhin.js/plugin-runtime": "1.1.1",
46
+ "@zhin.js/adapter": "1.1.3",
47
+ "@zhin.js/agent": "1.1.0",
50
48
  "@zhin.js/host-http": "1.0.4",
51
- "zhin.js": "5.0.3"
49
+ "@zhin.js/core": "1.5.0",
50
+ "@zhin.js/plugin-runtime": "1.1.1",
51
+ "zhin.js": "6.0.0"
52
52
  },
53
53
  "peerDependenciesMeta": {
54
54
  "zhin.js": {
@@ -66,8 +66,8 @@
66
66
  "typescript": "^6.0.3",
67
67
  "vitest": "^4.1.10",
68
68
  "zod": "^4.4.3",
69
- "@zhin.js/agent": "1.0.10",
70
- "zhin.js": "5.0.3"
69
+ "zhin.js": "6.0.0",
70
+ "@zhin.js/agent": "1.1.0"
71
71
  },
72
72
  "files": [
73
73
  "adapters",
package/plugin.js CHANGED
@@ -9,7 +9,7 @@ export default definePlugin({
9
9
  displayName: 'WeCom (企业微信) Adapter',
10
10
  },
11
11
  setup(context) {
12
- // 运行中 endpoint 注册表(wecom endpoint list 的"运行中"数据源)
12
+ // 运行中 endpoint 注册表(wecom.endpoint list 的"运行中"数据源)
13
13
  context.resources.provide(wecomRuntimeStateToken, createEndpointRuntimeState());
14
14
  return registerWecomPlatformPermitChecker();
15
15
  },
package/src/endpoint.ts CHANGED
@@ -143,18 +143,31 @@ export class WecomEndpoint implements EndpointInstance {
143
143
  }
144
144
 
145
145
  /**
146
- * message/send 的 image 段只接受 media_id:canonical MediaRef(base64/本地路径/URL)
147
- * 先经 /cgi-bin/media/upload 物化;上传失败降级为文本(alt 优先),不阻断发送。
146
+ * message/send 的 image 段只接受 media_id:canonical MediaRef 按 kind 投递——
147
+ * file(平台不透明引用)直用为 media_id;base64/本地路径/URL 先经
148
+ * /cgi-bin/media/upload 物化;上传失败降级为文本(alt 优先),不阻断发送。
149
+ * 无 canonical MediaRef 的媒体段 warn + 丢弃。
148
150
  */
149
151
  async #materializeOutboundMedia(payload: unknown): Promise<unknown> {
150
152
  if (!Array.isArray(payload)) return payload;
151
- return Promise.all(payload.map(async (item) => {
153
+ const items = await Promise.all(payload.map(async (item) => {
152
154
  if (typeof item === 'string' || !item || typeof item !== 'object') return item;
153
155
  const seg = item as { type?: unknown; data?: Record<string, unknown> };
154
156
  if (seg.type !== 'image') return item;
155
157
  const data = seg.data ?? {};
156
158
  const media = readOutboundImageMedia(data);
157
- if (!media) return item;
159
+ if (!media) {
160
+ logger.warn(formatCompact({
161
+ op: 'wecom_outbound_media_dropped',
162
+ endpoint: this.#options.config.name,
163
+ type: 'image',
164
+ reason: 'missing_media_ref',
165
+ }));
166
+ return null;
167
+ }
168
+ if (media.kind === 'file') {
169
+ return { type: 'image', data: { media_id: media.value } };
170
+ }
158
171
  try {
159
172
  const mediaId = await this.#uploadMedia('image', media);
160
173
  return { type: 'image', data: { media_id: mediaId } };
@@ -168,6 +181,7 @@ export class WecomEndpoint implements EndpointInstance {
168
181
  return { type: 'text', data: { text: alt } };
169
182
  }
170
183
  }));
184
+ return items.filter((item) => item !== null);
171
185
  }
172
186
 
173
187
  /** POST /cgi-bin/media/upload(临时素材,3 天有效),返回 media_id。 */
@@ -5,7 +5,7 @@
5
5
  */
6
6
  import { readFile } from 'node:fs/promises';
7
7
  import { basename } from 'node:path';
8
- import { isMediaRef, mediaRefFromLegacyData, type MediaRef } from '@zhin.js/core';
8
+ import { isMediaRef, type MediaRef } from '@zhin.js/core';
9
9
 
10
10
  export interface MediaBinary {
11
11
  readonly data: Buffer;
@@ -46,7 +46,11 @@ export async function resolveMediaBinary(
46
46
  const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
47
47
  return { data: await readFile(path), mimeType, fileName: basename(path) };
48
48
  }
49
- return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
49
+ if (media.kind === 'url') {
50
+ return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
51
+ }
52
+ // kind=file 为平台不透明引用(media_id),无二进制可解,调用方应直用 value
53
+ throw new Error(`cannot resolve binary from media kind: ${media.kind}`);
50
54
  }
51
55
 
52
56
  async function defaultDownload(url: string): Promise<Buffer> {
@@ -66,12 +70,10 @@ export function buildMediaUploadForm(binary: MediaBinary): FormData {
66
70
  }
67
71
 
68
72
  /**
69
- * 出站 image 段的媒体引用:已有 media_id 的视为已物化;
70
- * 否则读 canonical `data.media`,兼容旧 wire `{url,file,base64}` 字段。
73
+ * 出站 image 段的媒体引用:只读 canonical `data.media`(MediaRef)。
74
+ * 中央 normalizeOutboundPayload 已保证到达 endpoint 的载荷为 canonical;
75
+ * 无 MediaRef 时返回 undefined,由调用方 warn + 丢弃。
71
76
  */
72
77
  export function readOutboundImageMedia(data: Record<string, unknown>): MediaRef | undefined {
73
- if (typeof data.media_id === 'string' && data.media_id) return undefined;
74
- if (typeof data.mediaId === 'string' && data.mediaId) return undefined;
75
- if (isMediaRef(data.media)) return data.media;
76
- return mediaRefFromLegacyData(data);
78
+ return isMediaRef(data.media) ? data.media : undefined;
77
79
  }
package/src/protocol.ts CHANGED
@@ -292,8 +292,8 @@ export function formatOutboundBody(payload: unknown): WecomSendBody {
292
292
  if (!hasMedia) {
293
293
  hasMedia = true;
294
294
  mediaType = 'image';
295
- // media_id 由 endpoint 上传物化写入;file/url 为旧调用方直传 media_id 的兼容入口。
296
- mediaData = { media_id: data.media_id ?? data.mediaId ?? data.file ?? data.url };
295
+ // media_id 由 endpoint #materializeOutboundMedia 物化写入(内部 wire)。
296
+ mediaData = { media_id: data.media_id };
297
297
  }
298
298
  break;
299
299
  case 'markdown':
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `wecom endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `wecom.endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
3
3
  * commands/endpoint/ 下的 list / add / remove 直接默认导出这三项。
4
4
  */
5
5
  import { createEndpointCommands } from '@zhin.js/adapter';
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * WeCom 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `wecom endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `wecom.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6