@zhin.js/adapter-line 3.0.4 → 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,40 @@
1
1
  # @zhin.js/adapter-line
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
+
28
+ ## 3.0.5
29
+
30
+ ### Patch Changes
31
+
32
+ - Updated dependencies [d8bf702]
33
+ - @zhin.js/host-http@1.0.4
34
+ - @zhin.js/agent@1.0.10
35
+ - @zhin.js/core@1.4.3
36
+ - zhin.js@5.0.3
37
+
3
38
  ## 3.0.4
4
39
 
5
40
  ### Patch Changes
package/adapters/line.js CHANGED
@@ -18,7 +18,7 @@ export default defineAdapter({
18
18
  },
19
19
  create(context) {
20
20
  const config = resolveLineConfig(context.config);
21
- // 注册到插件运行时状态(line endpoint list 的"运行中"数据源)
21
+ // 注册到插件运行时状态(line.endpoint list 的"运行中"数据源)
22
22
  context.use(lineRuntimeStateToken).endpoints.set(config.name, {
23
23
  name: config.name,
24
24
  mode: 'webhook',
package/adapters/line.ts CHANGED
@@ -23,7 +23,7 @@ export default defineAdapter<LineAdapterConfig>({
23
23
  },
24
24
  create(context) {
25
25
  const config = resolveLineConfig(context.config);
26
- // 注册到插件运行时状态(line endpoint list 的"运行中"数据源)
26
+ // 注册到插件运行时状态(line.endpoint list 的"运行中"数据源)
27
27
  context.use(lineRuntimeStateToken).endpoints.set(config.name, {
28
28
  name: config.name,
29
29
  mode: 'webhook',
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `line endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `line.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
  * LINE 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `line endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `line.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6
  export const lineRuntimeStateToken = defineEndpointRuntimeStateToken('line');
package/lib/protocol.js CHANGED
@@ -3,6 +3,9 @@
3
3
  * Canonicalization is owned by gateway/core before endpoint.send.
4
4
  */
5
5
  import { createHmac, timingSafeEqual } from 'node:crypto';
6
+ import { isMediaRef } from '@zhin.js/core';
7
+ import { formatCompact, getLogger } from '@zhin.js/logger';
8
+ const logger = getLogger('line');
6
9
  export function resolveLineConfig(config = {}) {
7
10
  const entry = config.endpoints?.find((item) => item.context === 'line');
8
11
  const channelSecret = config.channelSecret
@@ -108,6 +111,32 @@ function buildTextMessage(text) {
108
111
  const truncated = text.length > 5000 ? `${text.slice(0, 4997)}...` : text;
109
112
  return { type: 'text', text: truncated };
110
113
  }
114
+ /**
115
+ * 解析 image/audio/video 段的投递源(canonical MediaRef 唯一来源)。
116
+ * LINE Messaging API 媒体消息仅消费远程 HTTPS URL(无上传通道),
117
+ * 因此只有 kind=url 可投递;base64 / path / file 一律 warn + 丢弃。
118
+ */
119
+ function resolveMediaUrl(seg) {
120
+ const media = (seg.data ?? {}).media;
121
+ if (!isMediaRef(media)) {
122
+ logger.warn(formatCompact({
123
+ op: 'line_outbound_media_dropped',
124
+ type: seg.type,
125
+ reason: 'missing_media_ref',
126
+ }));
127
+ return undefined;
128
+ }
129
+ if (media.kind !== 'url') {
130
+ logger.warn(formatCompact({
131
+ op: 'line_outbound_media_dropped',
132
+ type: seg.type,
133
+ reason: 'unsupported_media_kind',
134
+ kind: media.kind,
135
+ }));
136
+ return undefined;
137
+ }
138
+ return media.value;
139
+ }
111
140
  /**
112
141
  * Wire-encode an already-rendered outbound payload into LINE Reply/Push messages.
113
142
  * Segment canonicalization is intentionally not done here.
@@ -145,33 +174,39 @@ export function formatOutboundMessages(payload) {
145
174
  messages.push(buildTextMessage(`@${String(data.name || data.id)}`));
146
175
  }
147
176
  break;
148
- case 'image':
149
- if (typeof data.url === 'string' && data.url) {
177
+ case 'image': {
178
+ const url = resolveMediaUrl(item);
179
+ if (url) {
150
180
  messages.push({
151
181
  type: 'image',
152
- originalContentUrl: data.url,
153
- previewImageUrl: data.url,
182
+ originalContentUrl: url,
183
+ previewImageUrl: url,
154
184
  });
155
185
  }
156
186
  break;
157
- case 'video':
158
- if (typeof data.url === 'string' && data.url) {
187
+ }
188
+ case 'video': {
189
+ const url = resolveMediaUrl(item);
190
+ if (url) {
159
191
  messages.push({
160
192
  type: 'video',
161
- originalContentUrl: data.url,
162
- previewImageUrl: typeof data.previewUrl === 'string' ? data.previewUrl : data.url,
193
+ originalContentUrl: url,
194
+ previewImageUrl: typeof data.previewUrl === 'string' ? data.previewUrl : url,
163
195
  });
164
196
  }
165
197
  break;
166
- case 'audio':
167
- if (typeof data.url === 'string' && data.url) {
198
+ }
199
+ case 'audio': {
200
+ const url = resolveMediaUrl(item);
201
+ if (url) {
168
202
  messages.push({
169
203
  type: 'audio',
170
- originalContentUrl: data.url,
204
+ originalContentUrl: url,
171
205
  duration: typeof data.duration === 'number' ? data.duration : 0,
172
206
  });
173
207
  }
174
208
  break;
209
+ }
175
210
  case 'location':
176
211
  messages.push({
177
212
  type: 'location',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-line",
3
- "version": "3.0.4",
3
+ "version": "4.0.0",
4
4
  "description": "Zhin.js LINE Messaging API adapter for Plugin Runtime (HTTP webhook)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -32,21 +32,21 @@
32
32
  "directory": "plugins/adapters/line"
33
33
  },
34
34
  "dependencies": {
35
- "@zhin.js/adapter": "1.1.2",
36
- "@zhin.js/command": "1.0.4",
37
- "@zhin.js/core": "1.4.3",
38
- "@zhin.js/host-http": "1.0.3",
35
+ "@zhin.js/adapter": "1.1.3",
36
+ "@zhin.js/command": "1.0.5",
37
+ "@zhin.js/core": "1.5.0",
38
+ "@zhin.js/host-http": "1.0.4",
39
39
  "@zhin.js/logger": "1.0.75",
40
40
  "@zhin.js/plugin-runtime": "1.1.1"
41
41
  },
42
42
  "peerDependencies": {
43
43
  "zod": "^4.0.0",
44
- "@zhin.js/adapter": "1.1.2",
45
- "@zhin.js/agent": "1.0.9",
46
- "@zhin.js/core": "1.4.3",
47
- "@zhin.js/host-http": "1.0.3",
44
+ "@zhin.js/adapter": "1.1.3",
45
+ "@zhin.js/agent": "1.1.0",
46
+ "@zhin.js/core": "1.5.0",
47
+ "@zhin.js/host-http": "1.0.4",
48
48
  "@zhin.js/plugin-runtime": "1.1.1",
49
- "zhin.js": "5.0.3"
49
+ "zhin.js": "6.0.0"
50
50
  },
51
51
  "peerDependenciesMeta": {
52
52
  "zhin.js": {
@@ -60,11 +60,11 @@
60
60
  }
61
61
  },
62
62
  "devDependencies": {
63
- "@types/node": "^26.1.0",
63
+ "@types/node": "^26.1.2",
64
64
  "typescript": "^6.0.3",
65
65
  "vitest": "^4.1.10",
66
66
  "zod": "^4.4.3",
67
- "@zhin.js/agent": "1.0.9"
67
+ "@zhin.js/agent": "1.1.0"
68
68
  },
69
69
  "files": [
70
70
  "adapters",
package/plugin.js CHANGED
@@ -8,7 +8,7 @@ export default definePlugin({
8
8
  displayName: 'LINE Messaging API Adapter',
9
9
  },
10
10
  setup(context) {
11
- // 运行中 endpoint 注册表(line endpoint list 的"运行中"数据源)
11
+ // 运行中 endpoint 注册表(line.endpoint list 的"运行中"数据源)
12
12
  context.resources.provide(lineRuntimeStateToken, createEndpointRuntimeState());
13
13
  },
14
14
  });
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `line endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `line.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
  * LINE 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `line endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `line.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6
 
package/src/protocol.ts CHANGED
@@ -5,6 +5,10 @@
5
5
 
6
6
  import { createHmac, timingSafeEqual } from 'node:crypto';
7
7
  import type { IncomingMessage } from 'node:http';
8
+ import { isMediaRef } from '@zhin.js/core';
9
+ import { formatCompact, getLogger } from '@zhin.js/logger';
10
+
11
+ const logger = getLogger('line');
8
12
 
9
13
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
10
14
  export interface LineAdapterConfig {
@@ -272,6 +276,33 @@ function buildTextMessage(text: string): LineReplyMessage {
272
276
  return { type: 'text', text: truncated };
273
277
  }
274
278
 
279
+ /**
280
+ * 解析 image/audio/video 段的投递源(canonical MediaRef 唯一来源)。
281
+ * LINE Messaging API 媒体消息仅消费远程 HTTPS URL(无上传通道),
282
+ * 因此只有 kind=url 可投递;base64 / path / file 一律 warn + 丢弃。
283
+ */
284
+ function resolveMediaUrl(seg: LineWireSegment): string | undefined {
285
+ const media = (seg.data ?? {}).media;
286
+ if (!isMediaRef(media)) {
287
+ logger.warn(formatCompact({
288
+ op: 'line_outbound_media_dropped',
289
+ type: seg.type,
290
+ reason: 'missing_media_ref',
291
+ }));
292
+ return undefined;
293
+ }
294
+ if (media.kind !== 'url') {
295
+ logger.warn(formatCompact({
296
+ op: 'line_outbound_media_dropped',
297
+ type: seg.type,
298
+ reason: 'unsupported_media_kind',
299
+ kind: media.kind,
300
+ }));
301
+ return undefined;
302
+ }
303
+ return media.value;
304
+ }
305
+
275
306
  /**
276
307
  * Wire-encode an already-rendered outbound payload into LINE Reply/Push messages.
277
308
  * Segment canonicalization is intentionally not done here.
@@ -312,33 +343,39 @@ export function formatOutboundMessages(payload: unknown): LineReplyMessage[] {
312
343
  messages.push(buildTextMessage(`@${String(data.name || data.id)}`));
313
344
  }
314
345
  break;
315
- case 'image':
316
- if (typeof data.url === 'string' && data.url) {
346
+ case 'image': {
347
+ const url = resolveMediaUrl(item);
348
+ if (url) {
317
349
  messages.push({
318
350
  type: 'image',
319
- originalContentUrl: data.url,
320
- previewImageUrl: data.url,
351
+ originalContentUrl: url,
352
+ previewImageUrl: url,
321
353
  });
322
354
  }
323
355
  break;
324
- case 'video':
325
- if (typeof data.url === 'string' && data.url) {
356
+ }
357
+ case 'video': {
358
+ const url = resolveMediaUrl(item);
359
+ if (url) {
326
360
  messages.push({
327
361
  type: 'video',
328
- originalContentUrl: data.url,
329
- previewImageUrl: typeof data.previewUrl === 'string' ? data.previewUrl : data.url,
362
+ originalContentUrl: url,
363
+ previewImageUrl: typeof data.previewUrl === 'string' ? data.previewUrl : url,
330
364
  });
331
365
  }
332
366
  break;
333
- case 'audio':
334
- if (typeof data.url === 'string' && data.url) {
367
+ }
368
+ case 'audio': {
369
+ const url = resolveMediaUrl(item);
370
+ if (url) {
335
371
  messages.push({
336
372
  type: 'audio',
337
- originalContentUrl: data.url,
373
+ originalContentUrl: url,
338
374
  duration: typeof data.duration === 'number' ? data.duration : 0,
339
375
  });
340
376
  }
341
377
  break;
378
+ }
342
379
  case 'location':
343
380
  messages.push({
344
381
  type: 'location',