@zhin.js/adapter-kook 6.0.5 → 7.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-kook
2
2
 
3
+ ## 7.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
+ ## 6.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
  ## 6.0.5
4
48
 
5
49
  ### Patch Changes
package/adapters/kook.js CHANGED
@@ -20,7 +20,7 @@ export default defineAdapter({
20
20
  create(context) {
21
21
  const config = resolveKookConfig(context.config);
22
22
  const gateway = context.use(messageGatewayToken);
23
- // 注册到插件运行时状态(kook endpoint list 的"运行中"数据源)
23
+ // 注册到插件运行时状态(kook.endpoint list 的"运行中"数据源)
24
24
  context.use(kookRuntimeStateToken).endpoints.set(config.name, {
25
25
  name: config.name,
26
26
  mode: config.connection,
package/adapters/kook.ts CHANGED
@@ -35,7 +35,7 @@ export default defineAdapter<KookAdapterConfig>({
35
35
  create(context) {
36
36
  const config = resolveKookConfig(context.config);
37
37
  const gateway = context.use(messageGatewayToken);
38
- // 注册到插件运行时状态(kook endpoint list 的"运行中"数据源)
38
+ // 注册到插件运行时状态(kook.endpoint list 的"运行中"数据源)
39
39
  context.use(kookRuntimeStateToken).endpoints.set(config.name, {
40
40
  name: config.name,
41
41
  mode: config.connection,
package/lib/endpoint.d.ts CHANGED
@@ -24,6 +24,7 @@ export declare class KookWebsocketEndpoint implements EndpointInstance {
24
24
  }): Promise<string>;
25
25
  /** Test / internal: admit a message when open. */
26
26
  admit(msg: KookInboundMessage): void;
27
+ recallMessage(messageId: string): Promise<void>;
27
28
  getRoleList(guildId: string): Promise<{
28
29
  role_id: string | number;
29
30
  name: string;
@@ -66,6 +67,7 @@ export declare class KookWebhookEndpoint implements EndpointInstance {
66
67
  }): Promise<string>;
67
68
  /** Test / internal: admit a message when open. */
68
69
  admit(msg: KookInboundMessage): void;
70
+ recallMessage(messageId: string): Promise<void>;
69
71
  checkAndRememberSn(sn: number): boolean;
70
72
  getRoleList(guildId: string): Promise<{
71
73
  role_id: string | number;
package/lib/endpoint.js CHANGED
@@ -107,6 +107,11 @@ export class KookWebsocketEndpoint {
107
107
  }));
108
108
  });
109
109
  }
110
+ async recallMessage(messageId) {
111
+ if (!messageId)
112
+ return;
113
+ await this.#requireClient().recallMsg?.(messageId);
114
+ }
110
115
  // ── Agent tool surface ──────────────────────────────────────────────
111
116
  async getRoleList(guildId) {
112
117
  return this.#requireClient().pickGuild(guildId).getRoleList();
@@ -261,6 +266,11 @@ export class KookWebhookEndpoint {
261
266
  }));
262
267
  });
263
268
  }
269
+ async recallMessage(messageId) {
270
+ if (!messageId)
271
+ return;
272
+ await this.#requireClient().recallMsg?.(messageId);
273
+ }
264
274
  checkAndRememberSn(sn) {
265
275
  if (this.#processedSn.has(sn))
266
276
  return false;
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `kook endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `kook.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
  * KOOK 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `kook endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `kook.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6
  export const kookRuntimeStateToken = defineEndpointRuntimeStateToken('kook');
package/lib/protocol.js CHANGED
@@ -5,6 +5,9 @@
5
5
  import { createDecipheriv, timingSafeEqual } from 'node:crypto';
6
6
  import { inflateSync } from 'node:zlib';
7
7
  import { pickCredential } from '@zhin.js/adapter';
8
+ import { isMediaRef } from '@zhin.js/core';
9
+ import { formatCompact, getLogger } from '@zhin.js/logger';
10
+ const logger = getLogger('kook');
8
11
  export var KookPermission;
9
12
  (function (KookPermission) {
10
13
  KookPermission[KookPermission["Normal"] = 1] = "Normal";
@@ -95,6 +98,32 @@ export function isKookBotMentioned(msg, selfId) {
95
98
  const marker = `(met)${selfId}(met)`;
96
99
  return (msg.content || '').includes(marker) || (msg.rawMessage || '').includes(marker);
97
100
  }
101
+ /**
102
+ * 解析 image/audio/video/file 段的投递 URL(canonical MediaRef 唯一来源)。
103
+ * KOOK KMarkdown 媒体只能消费远程 URL;base64 / path / file 引用无法经
104
+ * KMarkdown 投递,warn 后丢弃(返回 undefined)。
105
+ */
106
+ function resolveOutboundMediaUrl(seg) {
107
+ const media = (seg.data ?? {}).media;
108
+ if (!isMediaRef(media)) {
109
+ logger.warn(formatCompact({
110
+ op: 'kook_outbound_media_dropped',
111
+ type: seg.type,
112
+ reason: 'missing_media_ref',
113
+ }));
114
+ return undefined;
115
+ }
116
+ if (media.kind !== 'url') {
117
+ logger.warn(formatCompact({
118
+ op: 'kook_outbound_media_dropped',
119
+ type: seg.type,
120
+ reason: 'unsupported_media_kind',
121
+ kind: media.kind,
122
+ }));
123
+ return undefined;
124
+ }
125
+ return media.value;
126
+ }
98
127
  /**
99
128
  * Wire-encode an already-rendered outbound payload into KOOK KMarkdown.
100
129
  * Segment canonicalization is intentionally not done here.
@@ -126,18 +155,26 @@ export function formatOutboundKmarkdown(payload) {
126
155
  const id = String(data.user_id ?? data.qq ?? data.id ?? '');
127
156
  return id === 'all' ? '(met)all(met)' : `(met)${id}(met)`;
128
157
  }
129
- case 'image':
130
- return `![${String(data.alt || '图片')}](${String(data.url || data.file || '')})`;
158
+ case 'image': {
159
+ const url = resolveOutboundMediaUrl(item);
160
+ return url ? `![${String(data.alt || '图片')}](${url})` : '';
161
+ }
131
162
  case 'face':
132
163
  return `(emj)${String(data.name || 'emoji')}(emj)[${String(data.id ?? '')}]`;
133
164
  case 'link':
134
165
  return `[${String(data.text || data.url || '')}](${String(data.url || '')})`;
135
- case 'video':
136
- return `[视频](${String(data.url || data.file || '')})`;
137
- case 'audio':
138
- return `[音频](${String(data.url || data.file || '')})`;
139
- case 'file':
140
- return `[文件: ${String(data.name || '未命名')}](${String(data.url || data.file || '')})`;
166
+ case 'video': {
167
+ const url = resolveOutboundMediaUrl(item);
168
+ return url ? `[视频](${url})` : '';
169
+ }
170
+ case 'audio': {
171
+ const url = resolveOutboundMediaUrl(item);
172
+ return url ? `[音频](${url})` : '';
173
+ }
174
+ case 'file': {
175
+ const url = resolveOutboundMediaUrl(item);
176
+ return url ? `[文件: ${String(data.name || '未命名')}](${url})` : '';
177
+ }
141
178
  case 'bold':
142
179
  return `**${String(data.text ?? '')}**`;
143
180
  case 'italic':
package/lib/ws.d.ts CHANGED
@@ -11,6 +11,7 @@ export interface KookClientTransport {
11
11
  sendPrivateMsg(userId: string, message: string): Promise<{
12
12
  msg_id?: string | number;
13
13
  }>;
14
+ recallMsg?(messageId: string): Promise<boolean>;
14
15
  pickGuild(guildId: string): {
15
16
  kick(userId: string): Promise<boolean>;
16
17
  getRoleList(): Promise<Array<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-kook",
3
- "version": "6.0.5",
3
+ "version": "7.0.0",
4
4
  "description": "Zhin.js KOOK adapter for Plugin Runtime (WebSocket gateway or Webhook)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -34,20 +34,20 @@
34
34
  },
35
35
  "dependencies": {
36
36
  "kook-client": "^1.0.4",
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/plugin-runtime": "1.1.1",
50
- "zhin.js": "5.0.3"
50
+ "zhin.js": "6.0.0"
51
51
  },
52
52
  "peerDependenciesMeta": {
53
53
  "zhin.js": {
@@ -65,9 +65,9 @@
65
65
  "typescript": "^6.0.3",
66
66
  "vitest": "^4.1.10",
67
67
  "zod": "^4.4.3",
68
- "@zhin.js/agent": "1.0.10",
68
+ "@zhin.js/agent": "1.1.0",
69
69
  "@zhin.js/host-http": "1.0.4",
70
- "zhin.js": "5.0.3"
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: 'KOOK WebSocket Adapter',
10
10
  },
11
11
  setup(context) {
12
- // 运行中 endpoint 注册表(kook endpoint list 的"运行中"数据源)
12
+ // 运行中 endpoint 注册表(kook.endpoint list 的"运行中"数据源)
13
13
  context.resources.provide(kookRuntimeStateToken, createEndpointRuntimeState());
14
14
  // 平台权限门禁:guild_owner / guild_admin / channel_admin 等(agent 工具 platformPermit)
15
15
  return registerKookPlatformPermitChecker();
package/src/endpoint.ts CHANGED
@@ -148,6 +148,11 @@ export class KookWebsocketEndpoint implements EndpointInstance {
148
148
  });
149
149
  }
150
150
 
151
+ async recallMessage(messageId: string): Promise<void> {
152
+ if (!messageId) return;
153
+ await this.#requireClient().recallMsg?.(messageId);
154
+ }
155
+
151
156
  // ── Agent tool surface ──────────────────────────────────────────────
152
157
 
153
158
  async getRoleList(guildId: string) {
@@ -323,6 +328,11 @@ export class KookWebhookEndpoint implements EndpointInstance {
323
328
  });
324
329
  }
325
330
 
331
+ async recallMessage(messageId: string): Promise<void> {
332
+ if (!messageId) return;
333
+ await this.#requireClient().recallMsg?.(messageId);
334
+ }
335
+
326
336
  checkAndRememberSn(sn: number): boolean {
327
337
  if (this.#processedSn.has(sn)) return false;
328
338
  this.#processedSn.add(sn);
@@ -1,5 +1,5 @@
1
1
  /**
2
- * `kook endpoint` 命令族:由 @zhin.js/adapter 的 createEndpointCommands 套件生成。
2
+ * `kook.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
  * KOOK 插件实例的运行时状态:adapter create() 注册的 endpoint 列表。
3
- * 由 plugin.ts setup() provide,adapter create 与 `kook endpoint` 命令共享(同一 owner generation)。
3
+ * 由 plugin.ts setup() provide,adapter create 与 `kook.endpoint` 命令共享(同一 owner generation)。
4
4
  */
5
5
  import { defineEndpointRuntimeStateToken } from '@zhin.js/adapter';
6
6
 
package/src/protocol.ts CHANGED
@@ -7,6 +7,10 @@ import { createDecipheriv, timingSafeEqual } from 'node:crypto';
7
7
  import { inflateSync } from 'node:zlib';
8
8
  import type { IncomingMessage } from 'node:http';
9
9
  import { pickCredential } from '@zhin.js/adapter';
10
+ import { isMediaRef } from '@zhin.js/core';
11
+ import { formatCompact, getLogger } from '@zhin.js/logger';
12
+
13
+ const logger = getLogger('kook');
10
14
 
11
15
  export type LogLevel =
12
16
  | 'trace'
@@ -228,6 +232,33 @@ export function isKookBotMentioned(msg: KookInboundMessage, selfId?: string): bo
228
232
  return (msg.content || '').includes(marker) || (msg.rawMessage || '').includes(marker);
229
233
  }
230
234
 
235
+ /**
236
+ * 解析 image/audio/video/file 段的投递 URL(canonical MediaRef 唯一来源)。
237
+ * KOOK KMarkdown 媒体只能消费远程 URL;base64 / path / file 引用无法经
238
+ * KMarkdown 投递,warn 后丢弃(返回 undefined)。
239
+ */
240
+ function resolveOutboundMediaUrl(seg: KookWireSegment): string | undefined {
241
+ const media = (seg.data ?? {}).media;
242
+ if (!isMediaRef(media)) {
243
+ logger.warn(formatCompact({
244
+ op: 'kook_outbound_media_dropped',
245
+ type: seg.type,
246
+ reason: 'missing_media_ref',
247
+ }));
248
+ return undefined;
249
+ }
250
+ if (media.kind !== 'url') {
251
+ logger.warn(formatCompact({
252
+ op: 'kook_outbound_media_dropped',
253
+ type: seg.type,
254
+ reason: 'unsupported_media_kind',
255
+ kind: media.kind,
256
+ }));
257
+ return undefined;
258
+ }
259
+ return media.value;
260
+ }
261
+
231
262
  /**
232
263
  * Wire-encode an already-rendered outbound payload into KOOK KMarkdown.
233
264
  * Segment canonicalization is intentionally not done here.
@@ -260,18 +291,26 @@ export function formatOutboundKmarkdown(payload: unknown): string {
260
291
  const id = String(data.user_id ?? data.qq ?? data.id ?? '');
261
292
  return id === 'all' ? '(met)all(met)' : `(met)${id}(met)`;
262
293
  }
263
- case 'image':
264
- return `![${String(data.alt || '图片')}](${String(data.url || data.file || '')})`;
294
+ case 'image': {
295
+ const url = resolveOutboundMediaUrl(item);
296
+ return url ? `![${String(data.alt || '图片')}](${url})` : '';
297
+ }
265
298
  case 'face':
266
299
  return `(emj)${String(data.name || 'emoji')}(emj)[${String(data.id ?? '')}]`;
267
300
  case 'link':
268
301
  return `[${String(data.text || data.url || '')}](${String(data.url || '')})`;
269
- case 'video':
270
- return `[视频](${String(data.url || data.file || '')})`;
271
- case 'audio':
272
- return `[音频](${String(data.url || data.file || '')})`;
273
- case 'file':
274
- return `[文件: ${String(data.name || '未命名')}](${String(data.url || data.file || '')})`;
302
+ case 'video': {
303
+ const url = resolveOutboundMediaUrl(item);
304
+ return url ? `[视频](${url})` : '';
305
+ }
306
+ case 'audio': {
307
+ const url = resolveOutboundMediaUrl(item);
308
+ return url ? `[音频](${url})` : '';
309
+ }
310
+ case 'file': {
311
+ const url = resolveOutboundMediaUrl(item);
312
+ return url ? `[文件: ${String(data.name || '未命名')}](${url})` : '';
313
+ }
275
314
  case 'bold':
276
315
  return `**${String(data.text ?? '')}**`;
277
316
  case 'italic':
package/src/ws.ts CHANGED
@@ -24,6 +24,7 @@ export interface KookClientTransport {
24
24
  userId: string,
25
25
  message: string,
26
26
  ): Promise<{ msg_id?: string | number }>;
27
+ recallMsg?(messageId: string): Promise<boolean>;
27
28
  pickGuild(guildId: string): {
28
29
  kick(userId: string): Promise<boolean>;
29
30
  getRoleList(): Promise<Array<{