@zhin.js/adapter-kook 4.0.2 → 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.
Files changed (43) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/index.js +2 -2
  3. package/lib/adapter.d.ts +4 -1
  4. package/lib/adapter.d.ts.map +1 -1
  5. package/lib/adapter.js +8 -2
  6. package/lib/adapter.js.map +1 -1
  7. package/lib/endpoint.d.ts +1 -1
  8. package/lib/endpoint.d.ts.map +1 -1
  9. package/lib/endpoint.js +17 -4
  10. package/lib/endpoint.js.map +1 -1
  11. package/lib/index.js +3 -3
  12. package/lib/kook-side-events.d.ts +4 -1
  13. package/lib/kook-side-events.d.ts.map +1 -1
  14. package/lib/kook-side-events.js +43 -1
  15. package/lib/kook-side-events.js.map +1 -1
  16. package/lib/outbound-keyboard.d.ts +13 -0
  17. package/lib/outbound-keyboard.d.ts.map +1 -0
  18. package/lib/outbound-keyboard.js +63 -0
  19. package/lib/outbound-keyboard.js.map +1 -0
  20. package/lib/outbound-media.d.ts +1 -1
  21. package/lib/outbound-media.d.ts.map +1 -1
  22. package/lib/outbound-media.js +4 -48
  23. package/lib/outbound-media.js.map +1 -1
  24. package/lib/outbound-sendable.d.ts +2 -1
  25. package/lib/outbound-sendable.d.ts.map +1 -1
  26. package/lib/outbound-sendable.js +9 -0
  27. package/lib/outbound-sendable.js.map +1 -1
  28. package/lib/platform-permit.js +2 -2
  29. package/lib/segment-mapper.d.ts +2 -0
  30. package/lib/segment-mapper.d.ts.map +1 -0
  31. package/lib/segment-mapper.js +2 -0
  32. package/lib/segment-mapper.js.map +1 -0
  33. package/package.json +15 -15
  34. package/skills/kook/PERMITS.md +1 -1
  35. package/src/adapter.ts +9 -2
  36. package/src/endpoint.ts +21 -4
  37. package/src/index.ts +4 -4
  38. package/src/kook-side-events.ts +49 -1
  39. package/src/outbound-keyboard.ts +78 -0
  40. package/src/outbound-media.ts +11 -55
  41. package/src/outbound-sendable.ts +13 -1
  42. package/src/platform-permit.ts +2 -2
  43. package/src/segment-mapper.ts +1 -0
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-media.d.ts","sourceRoot":"","sources":["../src/outbound-media.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,cAAc,EAAkB,WAAW,EAAE,MAAM,SAAS,CAAC;AAI3E,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrD;AA2FD,sCAAsC;AACtC,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,EAAE,CAAC,CAM3B"}
1
+ {"version":3,"file":"outbound-media.d.ts","sourceRoot":"","sources":["../src/outbound-media.ts"],"names":[],"mappings":"AAAA;;GAEG;AACH,OAAO,KAAK,EAAE,cAAc,EAAkB,WAAW,EAAE,MAAM,SAAS,CAAC;AAS3E,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CACrD;AA0CD,sCAAsC;AACtC,wBAAsB,wBAAwB,CAC5C,QAAQ,EAAE,iBAAiB,EAC3B,OAAO,EAAE,WAAW,GACnB,OAAO,CAAC,cAAc,EAAE,CAAC,CAM3B"}
@@ -1,51 +1,7 @@
1
- const MEDIA_TYPES = new Set(["image", "audio", "video", "file"]);
1
+ import { asMessageElements, extractBase64Buffer, isMediaSegmentType, isRemoteUrl, resolveLocalMediaPath, } from "zhin.js";
2
2
  function isMessageSegment(seg) {
3
3
  return typeof seg.type === "string";
4
4
  }
5
- function isRemoteUrl(value) {
6
- return /^https?:\/\//i.test(value);
7
- }
8
- function asSegments(content) {
9
- if (typeof content === "string") {
10
- return [{ type: "text", data: { text: content } }];
11
- }
12
- if (!Array.isArray(content)) {
13
- return [content];
14
- }
15
- return content.map((item) => typeof item === "string" ? { type: "text", data: { text: item } } : item);
16
- }
17
- function extractBase64Buffer(data) {
18
- const url = typeof data.url === "string" ? data.url : undefined;
19
- const file = typeof data.file === "string" ? data.file : undefined;
20
- const base64 = typeof data.base64 === "string" ? data.base64 : undefined;
21
- if (url?.startsWith("base64://")) {
22
- return Buffer.from(url.slice(9), "base64");
23
- }
24
- if (url && /^data:[^/]+\/[^;]+;base64,/i.test(url)) {
25
- return Buffer.from(url.replace(/^data:[^/]+\/[^;]+;base64,/, ""), "base64");
26
- }
27
- if (file?.startsWith("base64://")) {
28
- return Buffer.from(file.slice(9), "base64");
29
- }
30
- if (base64) {
31
- const raw = base64.startsWith("base64://") ? base64.slice(9) : base64;
32
- return Buffer.from(raw, "base64");
33
- }
34
- return undefined;
35
- }
36
- function resolveLocalFile(data) {
37
- const url = typeof data.url === "string" ? data.url : undefined;
38
- const file = typeof data.file === "string" ? data.file : undefined;
39
- const candidate = file ?? url;
40
- if (!candidate || isRemoteUrl(candidate))
41
- return undefined;
42
- if (candidate.startsWith("base64://") || /^data:/.test(candidate))
43
- return undefined;
44
- if (candidate.startsWith("file://") || candidate.startsWith("/") || !candidate.includes("://")) {
45
- return candidate;
46
- }
47
- return undefined;
48
- }
49
5
  function withUploadedUrl(seg, uploadedUrl) {
50
6
  const data = { ...seg.data };
51
7
  data.url = uploadedUrl;
@@ -54,7 +10,7 @@ function withUploadedUrl(seg, uploadedUrl) {
54
10
  return { type: seg.type, data };
55
11
  }
56
12
  async function resolveMediaSegment(uploader, seg) {
57
- if (!MEDIA_TYPES.has(seg.type))
13
+ if (!isMediaSegmentType(seg.type))
58
14
  return seg;
59
15
  const data = seg.data;
60
16
  const url = typeof data.url === "string" ? data.url : undefined;
@@ -67,7 +23,7 @@ async function resolveMediaSegment(uploader, seg) {
67
23
  const uploaded = await uploader.uploadMedia(buffer);
68
24
  return withUploadedUrl(seg, uploaded);
69
25
  }
70
- const local = resolveLocalFile(data);
26
+ const local = resolveLocalMediaPath(data);
71
27
  if (local) {
72
28
  const uploaded = await uploader.uploadMedia(local);
73
29
  return withUploadedUrl(seg, uploaded);
@@ -76,7 +32,7 @@ async function resolveMediaSegment(uploader, seg) {
76
32
  }
77
33
  /** 将 base64 / 本地媒体上传为 KOOK 可访问 URL */
78
34
  export async function materializeOutboundMedia(uploader, content) {
79
- const segments = asSegments(content);
35
+ const segments = asMessageElements(content);
80
36
  return Promise.all(segments.map(async (item) => {
81
37
  if (!isMessageSegment(item))
82
38
  return item;
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-media.js","sourceRoot":"","sources":["../src/outbound-media.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"outbound-media.js","sourceRoot":"","sources":["../src/outbound-media.ts"],"names":[],"mappings":"AAIA,OAAO,EACL,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,WAAW,EACX,qBAAqB,GACtB,MAAM,SAAS,CAAC;AAMjB,SAAS,gBAAgB,CAAC,GAAmB;IAC3C,OAAO,OAAO,GAAG,CAAC,IAAI,KAAK,QAAQ,CAAC;AACtC,CAAC;AAED,SAAS,eAAe,CAAC,GAAmB,EAAE,WAAmB;IAC/D,MAAM,IAAI,GAAG,EAAE,GAAG,GAAG,CAAC,IAAI,EAA6B,CAAC;IACxD,IAAI,CAAC,GAAG,GAAG,WAAW,CAAC;IACvB,IAAI,CAAC,IAAI,GAAG,WAAW,CAAC;IACxB,OAAO,IAAI,CAAC,MAAM,CAAC;IACnB,OAAO,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,EAAoB,CAAC;AACpD,CAAC;AAED,KAAK,UAAU,mBAAmB,CAChC,QAA2B,EAC3B,GAAmB;IAEnB,IAAI,CAAC,kBAAkB,CAAC,GAAG,CAAC,IAAI,CAAC;QAAE,OAAO,GAAG,CAAC;IAE9C,MAAM,IAAI,GAAG,GAAG,CAAC,IAA+B,CAAC;IACjD,MAAM,GAAG,GAAG,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,CAAC;IAChE,MAAM,IAAI,GAAG,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS,CAAC;IACnE,IAAI,CAAC,GAAG,IAAI,WAAW,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,IAAI,IAAI,WAAW,CAAC,IAAI,CAAC,CAAC,EAAE,CAAC;QAC7D,OAAO,GAAG,CAAC;IACb,CAAC;IAED,MAAM,MAAM,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC;IACzC,IAAI,MAAM,EAAE,CAAC;QACX,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC;QACpD,OAAO,eAAe,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED,MAAM,KAAK,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAC1C,IAAI,KAAK,EAAE,CAAC;QACV,MAAM,QAAQ,GAAG,MAAM,QAAQ,CAAC,WAAW,CAAC,KAAK,CAAC,CAAC;QACnD,OAAO,eAAe,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACxC,CAAC;IAED,OAAO,GAAG,CAAC;AACb,CAAC;AAED,sCAAsC;AACtC,MAAM,CAAC,KAAK,UAAU,wBAAwB,CAC5C,QAA2B,EAC3B,OAAoB;IAEpB,MAAM,QAAQ,GAAG,iBAAiB,CAAC,OAAO,CAAC,CAAC;IAC5C,OAAO,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE;QAC7C,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;QACzC,OAAO,mBAAmB,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC,CAAC;AACN,CAAC"}
@@ -4,8 +4,9 @@
4
4
  */
5
5
  import { type MessageSegment as KookMessageSegment } from "kook-client";
6
6
  import type { MessageElement } from "zhin.js";
7
+ export type KookSendable = string | KookMessageSegment[] | Array<Record<string, unknown>>;
7
8
  /**
8
9
  * 单张图片(可带 reply)→ kook image 消息段(type=2)
9
10
  */
10
- export declare function convertToKookSendable(elements: MessageElement[], formatText: (content: MessageElement[]) => string): string | KookMessageSegment[];
11
+ export declare function convertToKookSendable(elements: MessageElement[], formatText: (content: MessageElement[]) => string): KookSendable;
11
12
  //# sourceMappingURL=outbound-sendable.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-sendable.d.ts","sourceRoot":"","sources":["../src/outbound-sendable.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAW,KAAK,cAAc,IAAI,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAM9C;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,cAAc,EAAE,EAC1B,UAAU,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,MAAM,GAChD,MAAM,GAAG,kBAAkB,EAAE,CA2C/B"}
1
+ {"version":3,"file":"outbound-sendable.d.ts","sourceRoot":"","sources":["../src/outbound-sendable.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,OAAO,EAAW,KAAK,cAAc,IAAI,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAG9C,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,kBAAkB,EAAE,GAAG,KAAK,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC;AAM1F;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,cAAc,EAAE,EAC1B,UAAU,EAAE,CAAC,OAAO,EAAE,cAAc,EAAE,KAAK,MAAM,GAChD,YAAY,CAoDd"}
@@ -3,6 +3,7 @@
3
3
  * 图片/音视频须用 segment 发送对应 msg_type,不能写成 Markdown ![alt](url)。
4
4
  */
5
5
  import { segment } from "kook-client";
6
+ import { coreKeyboardToKookCard, findKeyboardSegment } from "./outbound-keyboard.js";
6
7
  function mediaUrl(data) {
7
8
  return String(data.url ?? data.file ?? "");
8
9
  }
@@ -10,6 +11,14 @@ function mediaUrl(data) {
10
11
  * 单张图片(可带 reply)→ kook image 消息段(type=2)
11
12
  */
12
13
  export function convertToKookSendable(elements, formatText) {
14
+ const keyboardBundle = findKeyboardSegment(elements);
15
+ if (keyboardBundle) {
16
+ const { keyboard, rest } = keyboardBundle;
17
+ const nonReplyRest = rest.filter((el) => el.type !== "reply");
18
+ if (nonReplyRest.every((el) => el.type === "text" || el.type === "reply")) {
19
+ return coreKeyboardToKookCard(keyboard.data, nonReplyRest.filter((el) => el.type === "text"));
20
+ }
21
+ }
13
22
  const replies = [];
14
23
  const images = [];
15
24
  const videos = [];
@@ -1 +1 @@
1
- {"version":3,"file":"outbound-sendable.js","sourceRoot":"","sources":["../src/outbound-sendable.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"outbound-sendable.js","sourceRoot":"","sources":["../src/outbound-sendable.ts"],"names":[],"mappings":"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"}
@@ -5,8 +5,8 @@ export function platformPermit(perm) {
5
5
  return `platform(${ADAPTER},${perm})`;
6
6
  }
7
7
  const FACTORY_PERM_MAP = {
8
- group_admin: 'guild_admin',
9
- group_owner: 'guild_owner',
8
+ scene_admin: 'guild_admin',
9
+ scene_owner: 'guild_owner',
10
10
  };
11
11
  export function kookGroupPermitResolver(logicalPerm) {
12
12
  return platformPermit(FACTORY_PERM_MAP[logicalPerm] ?? logicalPerm);
@@ -0,0 +1,2 @@
1
+ export { toCanonicalSegments, fromCanonicalSegments } from 'zhin.js';
2
+ //# sourceMappingURL=segment-mapper.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segment-mapper.d.ts","sourceRoot":"","sources":["../src/segment-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC"}
@@ -0,0 +1,2 @@
1
+ export { toCanonicalSegments, fromCanonicalSegments } from 'zhin.js';
2
+ //# sourceMappingURL=segment-mapper.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"segment-mapper.js","sourceRoot":"","sources":["../src/segment-mapper.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,SAAS,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-kook",
3
- "version": "4.0.2",
3
+ "version": "5.0.1",
4
4
  "description": "Zhin.js adapter for KOOK (开黑啦)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -38,23 +38,23 @@
38
38
  "devDependencies": {
39
39
  "@types/react": "^19.2.17",
40
40
  "@types/react-dom": "^19.2.3",
41
- "lucide-react": "^1.17.0",
41
+ "lucide-react": "^1.22.0",
42
42
  "typescript": "^6.0.3",
43
- "@zhin.js/cli": "1.0.90",
44
- "@zhin.js/client": "2.0.3",
45
- "@zhin.js/contract": "1.0.1",
46
- "@zhin.js/host-api": "2.0.2",
47
- "@zhin.js/host-router": "2.0.0",
48
- "@zhin.js/logger": "0.1.71",
49
- "zhin.js": "4.0.1"
43
+ "@zhin.js/cli": "1.0.92",
44
+ "@zhin.js/client": "2.0.4",
45
+ "@zhin.js/contract": "1.0.2",
46
+ "@zhin.js/host-api": "2.0.4",
47
+ "@zhin.js/host-router": "2.0.2",
48
+ "zhin.js": "4.1.1",
49
+ "@zhin.js/logger": "1.0.73"
50
50
  },
51
51
  "peerDependencies": {
52
- "@zhin.js/client": "2.0.3",
53
- "@zhin.js/host-api": "2.0.2",
54
- "@zhin.js/contract": "1.0.1",
55
- "@zhin.js/host-router": "2.0.0",
56
- "@zhin.js/logger": "0.1.71",
57
- "zhin.js": "4.0.1"
52
+ "@zhin.js/client": "2.0.4",
53
+ "@zhin.js/host-api": "2.0.4",
54
+ "@zhin.js/contract": "1.0.2",
55
+ "@zhin.js/logger": "1.0.73",
56
+ "@zhin.js/host-router": "2.0.2",
57
+ "zhin.js": "4.1.1"
58
58
  },
59
59
  "peerDependenciesMeta": {
60
60
  "@zhin.js/client": {
@@ -20,4 +20,4 @@ KOOK `permission`:1=成员,2=管理员,4=频道主,5=频道管理员(
20
20
 
21
21
  ## 工厂映射
22
22
 
23
- `group_admin` → `guild_admin` · `group_owner` → `guild_owner`
23
+ `scene_admin` → `guild_admin` · `scene_owner` → `guild_owner`
package/src/adapter.ts CHANGED
@@ -4,9 +4,16 @@
4
4
  import { formatCompact, Adapter, Plugin } from 'zhin.js';
5
5
  import { KookEndpoint } from "./endpoint.js";
6
6
  import type { KookEndpointConfig } from "./types.js";
7
+ import type { OutboundRichSegmentPolicy } from "zhin.js";
7
8
 
8
9
  export class KookAdapter extends Adapter<KookEndpoint> {
9
10
  static override readonly capabilities = ['inbound', 'outbound'] as const;
11
+ static override outboundRichSegmentPolicy: OutboundRichSegmentPolicy = {
12
+ qrcode: 'image',
13
+ html: 'image',
14
+ markdown: 'origin',
15
+ };
16
+ static override interactivePolicy = 'native' as const;
10
17
 
11
18
  constructor(plugin: Plugin) {
12
19
  super(plugin, "kook", []);
@@ -16,9 +23,9 @@ export class KookAdapter extends Adapter<KookEndpoint> {
16
23
  return new KookEndpoint(this, config);
17
24
  }
18
25
 
19
- // ── IGroupManagement 标准群管方法 ──────────────────────────────────
26
+ // ── ISceneManagement 标准群管方法 ──────────────────────────────────
20
27
 
21
- async kickMember(endpointId: string, sceneId: string, userId: string) {
28
+ async removeMember(endpointId: string, sceneId: string, userId: string) {
22
29
  const endpoint = this.endpoints.get(endpointId);
23
30
  if (!endpoint) throw new Error(`Endpoint ${endpointId} 不存在`);
24
31
  return endpoint.kickUser(sceneId, userId);
package/src/endpoint.ts CHANGED
@@ -20,8 +20,8 @@ import {
20
20
  SendContent,
21
21
  MessageElement,
22
22
  segment,
23
- MessageType,
24
- } from "zhin.js";
23
+ expandInteractiveSegmentsInContent,
24
+ MessageType,} from 'zhin.js';
25
25
  import type { KookEndpointConfig, KookSenderInfo, KookRawMessage } from "./types.js";
26
26
  import { KookPermission } from "./types.js";
27
27
  import type { KookAdapter } from "./adapter.js";
@@ -31,6 +31,8 @@ import {
31
31
  enrichKookGatewayForPlugins,
32
32
  formatKookNotice,
33
33
  formatKookNoticeLog,
34
+ formatKookButtonClickMessage,
35
+ isKookButtonClickEvent,
34
36
  isKookNoticeGatewayEvent,
35
37
  resolveKookSideEventDedupeKey,
36
38
  type KookGatewayEvent,
@@ -55,6 +57,7 @@ import {
55
57
  import { materializeOutboundMedia } from "./outbound-media.js";
56
58
  import { uploadKookAsset } from "./kook-asset-upload.js";
57
59
  import { convertToKookSendable } from "./outbound-sendable.js";
60
+ import { fromCanonicalSegments, toCanonicalSegments } from './segment-mapper.js';
58
61
 
59
62
  export class KookEndpoint extends Client implements Endpoint<KookEndpointConfig, KookRawMessage> {
60
63
  $connected: boolean = false;
@@ -111,6 +114,17 @@ export class KookEndpoint extends Client implements Endpoint<KookEndpointConfig,
111
114
  const enriched = enrichKookGatewayForPlugins(raw);
112
115
  EventEmitter.prototype.emit.call(this.adapter, "kook.gateway", enriched);
113
116
 
117
+ if (isKookButtonClickEvent(raw)) {
118
+ const dedupeKey = resolveKookSideEventDedupeKey(raw, "gateway");
119
+ if (!this.inboundDeduper.shouldProcess(dedupeKey)) return;
120
+ const message = formatKookButtonClickMessage(raw, this.$id);
121
+ this.adapter.emit("message.receive", message);
122
+ this.pluginLogger.debug(
123
+ `KOOK 按钮点击 ${message.$channel.type}(${message.$channel.id}): ${message.$raw}`,
124
+ );
125
+ return;
126
+ }
127
+
114
128
  if (!isKookNoticeGatewayEvent(raw)) return;
115
129
 
116
130
  const dedupeKey = resolveKookSideEventDedupeKey(raw, "notice");
@@ -222,7 +236,7 @@ export class KookEndpoint extends Client implements Endpoint<KookEndpointConfig,
222
236
  ...(guildId ? { guild_id: guildId } : {}),
223
237
  },
224
238
 
225
- $content: this.parseMessageContent(msg.message),
239
+ $content: toCanonicalSegments(this.parseMessageContent(msg.message)),
226
240
  $raw: msg.raw_message,
227
241
  $timestamp: msg.timestamp,
228
242
 
@@ -708,7 +722,10 @@ export class KookEndpoint extends Client implements Endpoint<KookEndpointConfig,
708
722
  try {
709
723
  const { id, type, content } = options;
710
724
 
711
- const elements = await materializeOutboundMedia(this, content);
725
+ const expanded = expandInteractiveSegmentsInContent(content);
726
+ const canonical = toCanonicalSegments(Array.isArray(expanded) ? expanded : [expanded]);
727
+ const wire = fromCanonicalSegments(canonical);
728
+ const elements = await materializeOutboundMedia(this, wire);
712
729
  const kookContent = convertToKookSendable(
713
730
  elements,
714
731
  (els) => this.convertToKookFormat(els),
package/src/index.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * KOOK 适配器入口:类型扩展、导出、注册
3
3
  */
4
4
  import path from "node:path";
5
- import { usePlugin, type Plugin, type IGroupManagement, createGroupManagementTools, type ToolFeature } from "zhin.js";
5
+ import { usePlugin, type Plugin, type ISceneManagement, createSceneManagementTools, type ToolFeature } from "zhin.js";
6
6
  import { KookAdapter } from "./adapter.js";
7
7
  import { kookGroupPermitResolver, platformPermit, registerKookPlatformPermitChecker } from "./platform-permit.js";
8
8
  import { PageManager } from "@zhin.js/host-api";
@@ -44,12 +44,12 @@ provide({
44
44
  useContext('tool', 'kook', (toolService: ToolFeature, kook: KookAdapter) => {
45
45
  const disposers: (() => void)[] = [];
46
46
  disposers.push(registerKookPlatformPermitChecker());
47
- const groupTools = createGroupManagementTools(
48
- kook as unknown as IGroupManagement,
47
+ const sceneTools = createSceneManagementTools(
48
+ kook as unknown as ISceneManagement,
49
49
  'kook',
50
50
  { permitResolver: kookGroupPermitResolver, registerChecker: false },
51
51
  );
52
- disposers.push(...groupTools.map(t => toolService.addTool(t, plugin.name)));
52
+ disposers.push(...sceneTools.map(t => toolService.addTool(t, plugin.name)));
53
53
 
54
54
  disposers.push(toolService.addTool({
55
55
  name: 'kook_grant_role',
@@ -2,7 +2,7 @@
2
2
  * KOOK Gateway 非聊天入站:系统消息 (type=255) / BROADCAST → Notice
3
3
  * @see https://developer.kookapp.cn/doc/event/event-introduction
4
4
  */
5
- import { Notice, formatCompact, type NoticeType } from "zhin.js";
5
+ import { Notice, formatCompact, Message, type NoticeType } from "zhin.js";
6
6
 
7
7
  /** KOOK WebSocket s=0 时 d 字段(kook-client Receiver 入参) */
8
8
  export interface KookGatewayEvent {
@@ -200,3 +200,51 @@ export function formatKookNoticeLog(notice: ReturnType<typeof formatKookNotice>)
200
200
  endpoint: notice.$endpoint,
201
201
  });
202
202
  }
203
+
204
+ /** KOOK 卡片按钮点击(type=255, extra.type=message_btn_click) */
205
+ export function isKookButtonClickEvent(event: KookGatewayEvent): boolean {
206
+ return event.type === SYSTEM_MESSAGE_TYPE && event.extra?.type === 'message_btn_click';
207
+ }
208
+
209
+ export function formatKookButtonClickMessage(
210
+ event: KookGatewayEvent,
211
+ endpointName: string,
212
+ ): ReturnType<typeof Message.from<KookGatewayEvent>> {
213
+ const extra = event.extra ?? {};
214
+ const body = extra.body ?? {};
215
+ const payload = String(body.value ?? '');
216
+ const userInfo = body.user_info as { id?: string; username?: string; nickname?: string } | undefined;
217
+ const userId = String(body.user_id ?? userInfo?.id ?? event.author_id ?? '');
218
+ const userName = userInfo?.nickname ?? userInfo?.username ?? userId;
219
+ const sourceMessageId = String(body.msg_id ?? '');
220
+
221
+ let channelType: 'private' | 'channel' = 'private';
222
+ let channelId = String(body.target_id ?? event.target_id ?? userId);
223
+ if (body.channel_type === 'GROUP' || event.channel_type === 'GROUP') {
224
+ channelType = 'channel';
225
+ channelId = String(body.target_id ?? event.target_id ?? channelId);
226
+ } else if (body.channel_type === 'PERSON' || event.channel_type === 'PERSON') {
227
+ channelType = 'private';
228
+ channelId = String(body.target_id ?? event.target_id ?? userId);
229
+ }
230
+
231
+ return Message.from(event, {
232
+ $id: event.msg_id ?? `btn:${Date.now()}`,
233
+ $adapter: 'kook',
234
+ $endpoint: endpointName,
235
+ $sender: { id: userId, name: userName },
236
+ $channel: { id: channelId, type: channelType },
237
+ $content: [{
238
+ type: 'action',
239
+ data: {
240
+ id: payload,
241
+ payload,
242
+ sourceMessageId,
243
+ },
244
+ }],
245
+ $raw: payload,
246
+ $timestamp: event.msg_timestamp ?? Date.now(),
247
+ $recall: async () => {},
248
+ $reply: async () => '',
249
+ });
250
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * core keyboard 段 → KOOK 卡片消息(action-group + button)
3
+ */
4
+ import type { KeyboardSegmentData, MessageElement } from "zhin.js";
5
+ import { KeyboardSegment, isKeyboardSegment } from "zhin.js";
6
+
7
+ function asKeyboardElement(
8
+ el: MessageElement,
9
+ ): (MessageElement & { data: KeyboardSegmentData }) | null {
10
+ if (el instanceof KeyboardSegment) {
11
+ return { type: "keyboard", data: el.data };
12
+ }
13
+ if (isKeyboardSegment(el)) return el;
14
+ return null;
15
+ }
16
+
17
+ function textFromElements(elements: MessageElement[]): string {
18
+ return elements
19
+ .filter((el) => el.type === "text" && typeof el.data.text === "string")
20
+ .map((el) => el.data.text as string)
21
+ .join("\n")
22
+ .trim();
23
+ }
24
+
25
+ /** core keyboard 布局 → KOOK card[](可直接传给 sendChannelMsg / sendPrivateMsg) */
26
+ export function coreKeyboardToKookCard(
27
+ data: KeyboardSegmentData,
28
+ prefixElements: MessageElement[] = [],
29
+ ): Array<Record<string, unknown>> {
30
+ const modules: Array<Record<string, unknown>> = [];
31
+ const prefixText = textFromElements(prefixElements);
32
+ if (prefixText) {
33
+ modules.push({
34
+ type: "section",
35
+ text: { type: "plain-text", content: prefixText },
36
+ });
37
+ }
38
+
39
+ for (const row of data.rows ?? []) {
40
+ modules.push({
41
+ type: "action-group",
42
+ elements: row.map((btn) => ({
43
+ type: "button",
44
+ theme: btn.disabled ? "secondary" : "primary",
45
+ value: btn.payload,
46
+ click: "return-val",
47
+ text: { type: "plain-text", content: btn.label },
48
+ })),
49
+ });
50
+ }
51
+
52
+ return [{
53
+ type: "card",
54
+ theme: "secondary",
55
+ size: "sm",
56
+ modules,
57
+ }];
58
+ }
59
+
60
+ export function findKeyboardSegment(
61
+ elements: MessageElement[],
62
+ ): { keyboard: MessageElement & { data: KeyboardSegmentData }; rest: MessageElement[] } | null {
63
+ let keyboard: (MessageElement & { data: KeyboardSegmentData }) | undefined;
64
+ const rest: MessageElement[] = [];
65
+
66
+ for (const el of elements) {
67
+ const kb = asKeyboardElement(el);
68
+ if (kb) {
69
+ if (keyboard) return null;
70
+ keyboard = kb;
71
+ continue;
72
+ }
73
+ rest.push(el);
74
+ }
75
+
76
+ if (!keyboard) return null;
77
+ return { keyboard, rest };
78
+ }
@@ -1,9 +1,14 @@
1
1
  /**
2
- * KOOK 出站媒体:base64 / 本地文件须先走 /v3/asset/create 上传,再嵌入 KMarkdown。
2
+ * KOOK 出站 media:base64 / 本地文件须先走 /v3/asset/create 上传,再嵌入 KMarkdown。
3
3
  */
4
4
  import type { MessageElement, MessageSegment, SendContent } from "zhin.js";
5
-
6
- const MEDIA_TYPES = new Set(["image", "audio", "video", "file"]);
5
+ import {
6
+ asMessageElements,
7
+ extractBase64Buffer,
8
+ isMediaSegmentType,
9
+ isRemoteUrl,
10
+ resolveLocalMediaPath,
11
+ } from "zhin.js";
7
12
 
8
13
  export interface KookMediaUploader {
9
14
  uploadMedia(data: string | Buffer): Promise<string>;
@@ -13,55 +18,6 @@ function isMessageSegment(seg: MessageElement): seg is MessageSegment {
13
18
  return typeof seg.type === "string";
14
19
  }
15
20
 
16
- function isRemoteUrl(value: string): boolean {
17
- return /^https?:\/\//i.test(value);
18
- }
19
-
20
- function asSegments(content: SendContent): MessageElement[] {
21
- if (typeof content === "string") {
22
- return [{ type: "text", data: { text: content } }];
23
- }
24
- if (!Array.isArray(content)) {
25
- return [content];
26
- }
27
- return content.map((item) =>
28
- typeof item === "string" ? { type: "text", data: { text: item } } : item,
29
- );
30
- }
31
-
32
- function extractBase64Buffer(data: Record<string, unknown>): Buffer | undefined {
33
- const url = typeof data.url === "string" ? data.url : undefined;
34
- const file = typeof data.file === "string" ? data.file : undefined;
35
- const base64 = typeof data.base64 === "string" ? data.base64 : undefined;
36
-
37
- if (url?.startsWith("base64://")) {
38
- return Buffer.from(url.slice(9), "base64");
39
- }
40
- if (url && /^data:[^/]+\/[^;]+;base64,/i.test(url)) {
41
- return Buffer.from(url.replace(/^data:[^/]+\/[^;]+;base64,/, ""), "base64");
42
- }
43
- if (file?.startsWith("base64://")) {
44
- return Buffer.from(file.slice(9), "base64");
45
- }
46
- if (base64) {
47
- const raw = base64.startsWith("base64://") ? base64.slice(9) : base64;
48
- return Buffer.from(raw, "base64");
49
- }
50
- return undefined;
51
- }
52
-
53
- function resolveLocalFile(data: Record<string, unknown>): string | undefined {
54
- const url = typeof data.url === "string" ? data.url : undefined;
55
- const file = typeof data.file === "string" ? data.file : undefined;
56
- const candidate = file ?? url;
57
- if (!candidate || isRemoteUrl(candidate)) return undefined;
58
- if (candidate.startsWith("base64://") || /^data:/.test(candidate)) return undefined;
59
- if (candidate.startsWith("file://") || candidate.startsWith("/") || !candidate.includes("://")) {
60
- return candidate;
61
- }
62
- return undefined;
63
- }
64
-
65
21
  function withUploadedUrl(seg: MessageSegment, uploadedUrl: string): MessageSegment {
66
22
  const data = { ...seg.data } as Record<string, unknown>;
67
23
  data.url = uploadedUrl;
@@ -74,7 +30,7 @@ async function resolveMediaSegment(
74
30
  uploader: KookMediaUploader,
75
31
  seg: MessageSegment,
76
32
  ): Promise<MessageSegment> {
77
- if (!MEDIA_TYPES.has(seg.type)) return seg;
33
+ if (!isMediaSegmentType(seg.type)) return seg;
78
34
 
79
35
  const data = seg.data as Record<string, unknown>;
80
36
  const url = typeof data.url === "string" ? data.url : undefined;
@@ -89,7 +45,7 @@ async function resolveMediaSegment(
89
45
  return withUploadedUrl(seg, uploaded);
90
46
  }
91
47
 
92
- const local = resolveLocalFile(data);
48
+ const local = resolveLocalMediaPath(data);
93
49
  if (local) {
94
50
  const uploaded = await uploader.uploadMedia(local);
95
51
  return withUploadedUrl(seg, uploaded);
@@ -103,7 +59,7 @@ export async function materializeOutboundMedia(
103
59
  uploader: KookMediaUploader,
104
60
  content: SendContent,
105
61
  ): Promise<MessageElement[]> {
106
- const segments = asSegments(content);
62
+ const segments = asMessageElements(content);
107
63
  return Promise.all(segments.map(async (item) => {
108
64
  if (!isMessageSegment(item)) return item;
109
65
  return resolveMediaSegment(uploader, item);
@@ -4,6 +4,9 @@
4
4
  */
5
5
  import { segment, type MessageSegment as KookMessageSegment } from "kook-client";
6
6
  import type { MessageElement } from "zhin.js";
7
+ import { coreKeyboardToKookCard, findKeyboardSegment } from "./outbound-keyboard.js";
8
+
9
+ export type KookSendable = string | KookMessageSegment[] | Array<Record<string, unknown>>;
7
10
 
8
11
  function mediaUrl(data: Record<string, unknown>): string {
9
12
  return String(data.url ?? data.file ?? "");
@@ -15,7 +18,16 @@ function mediaUrl(data: Record<string, unknown>): string {
15
18
  export function convertToKookSendable(
16
19
  elements: MessageElement[],
17
20
  formatText: (content: MessageElement[]) => string,
18
- ): string | KookMessageSegment[] {
21
+ ): KookSendable {
22
+ const keyboardBundle = findKeyboardSegment(elements);
23
+ if (keyboardBundle) {
24
+ const { keyboard, rest } = keyboardBundle;
25
+ const nonReplyRest = rest.filter((el) => el.type !== "reply");
26
+ if (nonReplyRest.every((el) => el.type === "text" || el.type === "reply")) {
27
+ return coreKeyboardToKookCard(keyboard.data, nonReplyRest.filter((el) => el.type === "text"));
28
+ }
29
+ }
30
+
19
31
  const replies: string[] = [];
20
32
  const images: string[] = [];
21
33
  const videos: string[] = [];
@@ -13,8 +13,8 @@ export function platformPermit(perm: string): string {
13
13
  }
14
14
 
15
15
  const FACTORY_PERM_MAP: Record<string, string> = {
16
- group_admin: 'guild_admin',
17
- group_owner: 'guild_owner',
16
+ scene_admin: 'guild_admin',
17
+ scene_owner: 'guild_owner',
18
18
  };
19
19
 
20
20
  export function kookGroupPermitResolver(logicalPerm: string): string {
@@ -0,0 +1 @@
1
+ export { toCanonicalSegments, fromCanonicalSegments } from 'zhin.js';