@zhin.js/adapter-lark 5.0.5 → 6.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,49 @@
1
1
  # @zhin.js/adapter-lark
2
2
 
3
+ ## 6.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
+ ## 5.0.6
29
+
30
+ ### Patch Changes
31
+
32
+ - d52f3c5: fix: QQ 适配器群聊误识别为私聊、game-kit 启动崩溃、全面补齐 recallMessage
33
+
34
+ - fix(cli): `resolveChannelType` 增加 `metadata.channelKind` 检查,修复 QQ/Discord/KOOK 群聊消息被误分类为私聊
35
+ - fix(game-kit): `createHostGameDb` 改用延迟代理模型,避免数据库启动前解析模型导致崩溃
36
+ - feat(qq): 实现 `recallMessage`,解析复合 messageId 路由到对应 SDK 撤回方法;`normalizeQqMessage` 增加 `message_type` 缺失时的回退检测
37
+ - feat(slack): 实现 `recallMessage`,利用已有 compound ref 和 `chat.delete` API
38
+ - feat(onebot11): 两个 endpoint 类实现 `recallMessage`(`delete_msg`)
39
+ - feat(onebot12): 两个 endpoint 类实现 `recallMessage`(`delete_message`)
40
+ - feat(napcat): 三个 endpoint 类实现 `recallMessage`(`delete_msg`)
41
+ - feat(discord): 两个 endpoint 类实现 `recallMessage`;`send()` 改为返回 `channelId:snowflake` 复合 ID
42
+ - feat(telegram): 实现 `recallMessage`(`deleteMessage`);`send()` 改为返回 `chatId:messageId` 复合 ID
43
+ - feat(kook): 两个 endpoint 类实现 `recallMessage`,利用 kook-client `recallMsg` API
44
+ - feat(lark): 实现 `recallMessage`(`DELETE /im/v1/messages/{id}`)
45
+ - feat(wecom): 实现 `recallMessage`(`POST /cgi-bin/message/recall`)
46
+
3
47
  ## 5.0.5
4
48
 
5
49
  ### Patch Changes
package/adapters/lark.js CHANGED
@@ -11,7 +11,8 @@ import { larkRuntimeStateToken } from "../lib/lark-runtime-state.js";
11
11
  export { LarkEndpoint } from "../lib/endpoint.js";
12
12
  export default defineAdapter({
13
13
  capabilities: ['inbound', 'outbound'],
14
- // image 段经 /im/v1/images 物化为 image_key(url 下载后上传);
14
+ // image 段 canonical MediaRef 经 /im/v1/images 物化为 image_key
15
+ // (base64 解码 / path 读盘 / url 下载后上传,kind=file 平台引用直通);
15
16
  // 卡片交互未接入出站通道,交互段降级纯文本。
16
17
  segments: {
17
18
  outboundMedia: ['url', 'upload'],
@@ -19,7 +20,7 @@ export default defineAdapter({
19
20
  },
20
21
  create(context) {
21
22
  const config = resolveLarkConfig(context.config);
22
- // 注册到插件运行时状态(lark endpoint list 的"运行中"数据源)
23
+ // 注册到插件运行时状态(lark.endpoint list 的"运行中"数据源)
23
24
  context.use(larkRuntimeStateToken).endpoints.set(config.name, {
24
25
  name: config.name,
25
26
  mode: 'webhook',
package/adapters/lark.ts CHANGED
@@ -16,7 +16,8 @@ export type { LarkEndpointOptions, LarkFetch } from '../src/endpoint.js';
16
16
 
17
17
  export default defineAdapter<LarkAdapterConfig>({
18
18
  capabilities: ['inbound', 'outbound'],
19
- // image 段经 /im/v1/images 物化为 image_key(url 下载后上传);
19
+ // image 段 canonical MediaRef 经 /im/v1/images 物化为 image_key
20
+ // (base64 解码 / path 读盘 / url 下载后上传,kind=file 平台引用直通);
20
21
  // 卡片交互未接入出站通道,交互段降级纯文本。
21
22
  segments: {
22
23
  outboundMedia: ['url', 'upload'],
@@ -24,7 +25,7 @@ export default defineAdapter<LarkAdapterConfig>({
24
25
  },
25
26
  create(context) {
26
27
  const config = resolveLarkConfig(context.config);
27
- // 注册到插件运行时状态(lark endpoint list 的"运行中"数据源)
28
+ // 注册到插件运行时状态(lark.endpoint list 的"运行中"数据源)
28
29
  context.use(larkRuntimeStateToken).endpoints.set(config.name, {
29
30
  name: config.name,
30
31
  mode: 'webhook',
package/lib/endpoint.d.ts CHANGED
@@ -44,6 +44,7 @@ export declare class LarkEndpoint implements EndpointInstance {
44
44
  readonly target: string;
45
45
  readonly payload: unknown;
46
46
  }): Promise<string>;
47
+ recallMessage(messageId: string): Promise<void>;
47
48
  /** POST /im/v1/images(image_type=message),返回 image_key;失败返回 null。 */
48
49
  uploadImage(media: Parameters<typeof resolveMediaBinary>[0]): Promise<string | null>;
49
50
  /** Test / internal: admit a parsed message when open (non-webhook path). */
package/lib/endpoint.js CHANGED
@@ -123,9 +123,15 @@ export class LarkEndpoint {
123
123
  }));
124
124
  return messageId;
125
125
  }
126
+ async recallMessage(messageId) {
127
+ if (!messageId || messageId.startsWith('outbound:'))
128
+ return;
129
+ await this.#request(`/im/v1/messages/${messageId}`, { method: 'DELETE' });
130
+ }
126
131
  /**
127
132
  * im/v1 消息 image 段只接受 image_key:canonical MediaRef(base64/本地路径/URL)
128
- * 先经 /im/v1/images 物化;上传失败降级为文本(alt 优先),不阻断发送。
133
+ * 先经 /im/v1/images 物化,产物回写为 MediaRef kind=file(平台不透明引用);
134
+ * 上传失败降级为文本(alt 优先),不阻断发送。
129
135
  */
130
136
  async #materializeOutboundMedia(payload) {
131
137
  if (!Array.isArray(payload))
@@ -145,7 +151,10 @@ export class LarkEndpoint {
145
151
  const alt = typeof data.alt === 'string' && data.alt ? data.alt : '[image]';
146
152
  return { type: 'text', data: { text: alt } };
147
153
  }
148
- return { type: 'image', data: { image_key: imageKey } };
154
+ return {
155
+ type: 'image',
156
+ data: { media: { kind: 'file', value: imageKey } },
157
+ };
149
158
  }));
150
159
  }
151
160
  /** POST /im/v1/images(image_type=message),返回 image_key;失败返回 null。 */
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `lark endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `lark.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
  * Lark 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `lark endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `lark.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6
  export const larkRuntimeStateToken = defineEndpointRuntimeStateToken('lark');
@@ -9,7 +9,7 @@ export declare function resolveMediaBinary(media: MediaRef, download?: (url: str
9
9
  /** /im/v1/images 的 multipart body(image_type=message 用于消息内图片)。 */
10
10
  export declare function buildImageUploadForm(binary: MediaBinary): FormData;
11
11
  /**
12
- * 出站 image 段的媒体引用:已有 image_key/file_key 的视为已物化;
13
- * 否则读 canonical `data.media`,兼容旧 wire `{url,file,base64}` 字段。
12
+ * 出站 image 段待上传的媒体引用(canonical `data.media` 唯一来源):
13
+ * kind=file 为平台不透明引用(image_key/file_key),已物化无需再上传,返回 undefined。
14
14
  */
15
15
  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',
@@ -46,17 +46,13 @@ export function buildImageUploadForm(binary) {
46
46
  return form;
47
47
  }
48
48
  /**
49
- * 出站 image 段的媒体引用:已有 image_key/file_key 的视为已物化;
50
- * 否则读 canonical `data.media`,兼容旧 wire `{url,file,base64}` 字段。
49
+ * 出站 image 段待上传的媒体引用(canonical `data.media` 唯一来源):
50
+ * kind=file 为平台不透明引用(image_key/file_key),已物化无需再上传,返回 undefined。
51
51
  */
52
52
  export function readOutboundImageMedia(data) {
53
- if (typeof data.image_key === 'string' && data.image_key)
53
+ if (!isMediaRef(data.media))
54
54
  return undefined;
55
- if (typeof data.file_key === 'string' && data.file_key)
55
+ if (data.media.kind === 'file')
56
56
  return undefined;
57
- if (typeof data.key === 'string' && data.key)
58
- return undefined;
59
- if (isMediaRef(data.media))
60
- return data.media;
61
- return mediaRefFromLegacyData(data);
57
+ return data.media;
62
58
  }
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 { createHash, timingSafeEqual } from 'node:crypto';
6
+ import { isMediaRef } from '@zhin.js/core';
7
+ import { formatCompact, getLogger } from '@zhin.js/logger';
8
+ const logger = getLogger('lark');
6
9
  export function resolveLarkConfig(config = {}) {
7
10
  const entry = config.endpoints?.find((item) => item.context === 'lark');
8
11
  const appId = config.appId ?? entry?.appId ?? process.env.LARK_APP_ID;
@@ -169,23 +172,39 @@ export function formatOutboundBody(payload) {
169
172
  break;
170
173
  case 'image':
171
174
  if (!media) {
172
- media = {
173
- msg_type: 'image',
174
- content: JSON.stringify({
175
- // image_key 由 endpoint 上传物化写入;file_key/key/url 为旧调用方兼容入口。
176
- image_key: data.image_key ?? data.file_key ?? data.key ?? data.url,
177
- }),
178
- };
175
+ // image_key 唯一来源:canonical MediaRef kind=file(endpoint 上传物化产物或调用方既有平台引用)
176
+ const ref = data.media;
177
+ if (isMediaRef(ref) && ref.kind === 'file') {
178
+ media = {
179
+ msg_type: 'image',
180
+ content: JSON.stringify({ image_key: ref.value }),
181
+ };
182
+ }
183
+ else {
184
+ logger.warn(formatCompact({
185
+ op: 'lark_outbound_media_dropped',
186
+ type: 'image',
187
+ reason: isMediaRef(ref) ? 'unmaterialized_media' : 'missing_media_ref',
188
+ }));
189
+ }
179
190
  }
180
191
  break;
181
192
  case 'file':
182
193
  if (!media) {
183
- media = {
184
- msg_type: 'file',
185
- content: JSON.stringify({
186
- file_key: data.file_key ?? data.key,
187
- }),
188
- };
194
+ const ref = data.media;
195
+ if (isMediaRef(ref) && ref.kind === 'file') {
196
+ media = {
197
+ msg_type: 'file',
198
+ content: JSON.stringify({ file_key: ref.value }),
199
+ };
200
+ }
201
+ else {
202
+ logger.warn(formatCompact({
203
+ op: 'lark_outbound_media_dropped',
204
+ type: 'file',
205
+ reason: isMediaRef(ref) ? 'unmaterialized_media' : 'missing_media_ref',
206
+ }));
207
+ }
189
208
  }
190
209
  break;
191
210
  case 'card':
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-lark",
3
- "version": "5.0.5",
3
+ "version": "6.0.0",
4
4
  "description": "Zhin.js Lark/Feishu (飞书) 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/lark"
35
35
  },
36
36
  "dependencies": {
37
- "@zhin.js/adapter": "1.1.2",
38
- "@zhin.js/command": "1.0.4",
39
- "@zhin.js/core": "1.4.3",
37
+ "@zhin.js/adapter": "1.1.3",
38
+ "@zhin.js/command": "1.0.5",
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",
46
+ "@zhin.js/adapter": "1.1.3",
47
+ "@zhin.js/agent": "1.1.0",
48
+ "@zhin.js/core": "1.5.0",
49
49
  "@zhin.js/host-http": "1.0.4",
50
50
  "@zhin.js/plugin-runtime": "1.1.1",
51
- "zhin.js": "5.0.3"
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/agent": "1.1.0",
70
+ "zhin.js": "6.0.0"
71
71
  },
72
72
  "files": [
73
73
  "adapters",
package/plugin.js CHANGED
@@ -9,7 +9,7 @@ export default definePlugin({
9
9
  displayName: 'Lark/Feishu (飞书) Adapter',
10
10
  },
11
11
  setup(context) {
12
- // 运行中 endpoint 注册表(lark endpoint list 的"运行中"数据源)
12
+ // 运行中 endpoint 注册表(lark.endpoint list 的"运行中"数据源)
13
13
  context.resources.provide(larkRuntimeStateToken, createEndpointRuntimeState());
14
14
  return registerLarkPlatformPermitChecker();
15
15
  },
package/src/endpoint.ts CHANGED
@@ -176,9 +176,15 @@ export class LarkEndpoint implements EndpointInstance {
176
176
  return messageId;
177
177
  }
178
178
 
179
+ async recallMessage(messageId: string): Promise<void> {
180
+ if (!messageId || messageId.startsWith('outbound:')) return;
181
+ await this.#request(`/im/v1/messages/${messageId}`, { method: 'DELETE' });
182
+ }
183
+
179
184
  /**
180
185
  * im/v1 消息 image 段只接受 image_key:canonical MediaRef(base64/本地路径/URL)
181
- * 先经 /im/v1/images 物化;上传失败降级为文本(alt 优先),不阻断发送。
186
+ * 先经 /im/v1/images 物化,产物回写为 MediaRef kind=file(平台不透明引用);
187
+ * 上传失败降级为文本(alt 优先),不阻断发送。
182
188
  */
183
189
  async #materializeOutboundMedia(payload: unknown): Promise<unknown> {
184
190
  if (!Array.isArray(payload)) return payload;
@@ -194,7 +200,10 @@ export class LarkEndpoint implements EndpointInstance {
194
200
  const alt = typeof data.alt === 'string' && data.alt ? data.alt : '[image]';
195
201
  return { type: 'text', data: { text: alt } };
196
202
  }
197
- return { type: 'image', data: { image_key: imageKey } };
203
+ return {
204
+ type: 'image',
205
+ data: { media: { kind: 'file', value: imageKey } },
206
+ };
198
207
  }));
199
208
  }
200
209
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `lark endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `lark.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
  * Lark 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `lark endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `lark.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6
 
@@ -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;
@@ -59,13 +59,11 @@ export function buildImageUploadForm(binary: MediaBinary): FormData {
59
59
  }
60
60
 
61
61
  /**
62
- * 出站 image 段的媒体引用:已有 image_key/file_key 的视为已物化;
63
- * 否则读 canonical `data.media`,兼容旧 wire `{url,file,base64}` 字段。
62
+ * 出站 image 段待上传的媒体引用(canonical `data.media` 唯一来源):
63
+ * kind=file 为平台不透明引用(image_key/file_key),已物化无需再上传,返回 undefined。
64
64
  */
65
65
  export function readOutboundImageMedia(data: Record<string, unknown>): MediaRef | undefined {
66
- if (typeof data.image_key === 'string' && data.image_key) return undefined;
67
- if (typeof data.file_key === 'string' && data.file_key) return undefined;
68
- if (typeof data.key === 'string' && data.key) return undefined;
69
- if (isMediaRef(data.media)) return data.media;
70
- return mediaRefFromLegacyData(data);
66
+ if (!isMediaRef(data.media)) return undefined;
67
+ if (data.media.kind === 'file') return undefined;
68
+ return data.media;
71
69
  }
package/src/protocol.ts CHANGED
@@ -5,6 +5,10 @@
5
5
 
6
6
  import { createHash, 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('lark');
8
12
 
9
13
  /** Plugin Runtime owner config (`plugins.<instanceKey>` / schema.json). */
10
14
  export interface LarkAdapterConfig {
@@ -291,23 +295,37 @@ export function formatOutboundBody(payload: unknown): LarkSendBody {
291
295
  break;
292
296
  case 'image':
293
297
  if (!media) {
294
- media = {
295
- msg_type: 'image',
296
- content: JSON.stringify({
297
- // image_key 由 endpoint 上传物化写入;file_key/key/url 为旧调用方兼容入口。
298
- image_key: data.image_key ?? data.file_key ?? data.key ?? data.url,
299
- }),
300
- };
298
+ // image_key 唯一来源:canonical MediaRef kind=file(endpoint 上传物化产物或调用方既有平台引用)
299
+ const ref = data.media;
300
+ if (isMediaRef(ref) && ref.kind === 'file') {
301
+ media = {
302
+ msg_type: 'image',
303
+ content: JSON.stringify({ image_key: ref.value }),
304
+ };
305
+ } else {
306
+ logger.warn(formatCompact({
307
+ op: 'lark_outbound_media_dropped',
308
+ type: 'image',
309
+ reason: isMediaRef(ref) ? 'unmaterialized_media' : 'missing_media_ref',
310
+ }));
311
+ }
301
312
  }
302
313
  break;
303
314
  case 'file':
304
315
  if (!media) {
305
- media = {
306
- msg_type: 'file',
307
- content: JSON.stringify({
308
- file_key: data.file_key ?? data.key,
309
- }),
310
- };
316
+ const ref = data.media;
317
+ if (isMediaRef(ref) && ref.kind === 'file') {
318
+ media = {
319
+ msg_type: 'file',
320
+ content: JSON.stringify({ file_key: ref.value }),
321
+ };
322
+ } else {
323
+ logger.warn(formatCompact({
324
+ op: 'lark_outbound_media_dropped',
325
+ type: 'file',
326
+ reason: isMediaRef(ref) ? 'unmaterialized_media' : 'missing_media_ref',
327
+ }));
328
+ }
311
329
  }
312
330
  break;
313
331
  case 'card':