@zhin.js/adapter-lark 1.1.18 → 1.1.19

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/lib/index.d.ts CHANGED
@@ -1,7 +1,9 @@
1
1
  export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, larkInboundConversation, normalizeWebhookPath, readTextBody, resolveChatType, resolveLarkConfig, resolveSender, verifySignature, type AccessToken, type LarkEndpointConfig, type LarkApiResponse, type LarkEventBody, type LarkMessage, type LarkSendBody, type LarkWireSegment, type ResolvedLarkConfig, } from './protocol.js';
2
- export { larkClient, type LarkClientEventMap } from './client.js';
2
+ export { type LarkClientEventMap } from './client.js';
3
3
  export { checkLarkPlatformPermit, larkGroupPermitResolver, normalizeLarkSenderForPermit, platformPermit, } from './platform-permit.js';
4
- export { LarkClient, LarkEndpoint, type LarkClientApi, type LarkEndpointOptions, type LarkFetch, } from './endpoint.js';
4
+ export { LarkEndpoint, type LarkEndpointOptions, type LarkFetch, } from './endpoint.js';
5
5
  export { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, type MediaBinary, } from './media-upload.js';
6
6
  export { registerLarkWebhookRoutes, handleLarkWebhookRequest, type LarkWebhookHandler, } from './webhook.js';
7
7
  export { createLarkLongConnection, type LarkLongConnection, type LarkLongConnectionFactory, type LarkLongConnectionOptions, } from './long-connection.js';
8
+ export { LarkClient, type LarkClientApi } from './client.js';
9
+ export { LarkAdapter } from './adapter.js';
package/lib/index.js CHANGED
@@ -1,7 +1,8 @@
1
1
  export { formatInboundContent, formatOutboundBody, generateMessageId, headerValue, larkInboundConversation, normalizeWebhookPath, readTextBody, resolveChatType, resolveLarkConfig, resolveSender, verifySignature, } from './protocol.js';
2
- export { larkClient } from './client.js';
3
2
  export { checkLarkPlatformPermit, larkGroupPermitResolver, normalizeLarkSenderForPermit, platformPermit, } from './platform-permit.js';
4
- export { LarkClient, LarkEndpoint, } from './endpoint.js';
3
+ export { LarkEndpoint, } from './endpoint.js';
5
4
  export { buildImageUploadForm, readOutboundImageMedia, resolveMediaBinary, } from './media-upload.js';
6
5
  export { registerLarkWebhookRoutes, handleLarkWebhookRequest, } from './webhook.js';
7
6
  export { createLarkLongConnection, } from './long-connection.js';
7
+ export { LarkClient } from './client.js';
8
+ export { LarkAdapter } from './adapter.js';
@@ -7,7 +7,7 @@ export interface LarkLongConnection {
7
7
  }
8
8
  export interface LarkLongConnectionOptions {
9
9
  readonly config: ResolvedLarkConfig;
10
- readonly receive: (message: LarkMessage) => void;
10
+ readonly receive: (message: LarkMessage, nativeEvent?: unknown) => void;
11
11
  readonly cardAction?: (event: LarkCardAction) => void;
12
12
  readonly disconnected: () => void;
13
13
  }
@@ -42,7 +42,7 @@ export const createLarkLongConnection = ({ config, receive, cardAction, disconne
42
42
  cardAction?.(event); return {}; },
43
43
  'im.message.receive_v1': (event) => {
44
44
  if (!cancelled)
45
- receive({ ...event.message, sender: event.sender });
45
+ receive({ ...event.message, sender: event.sender }, event);
46
46
  // No downstream send is awaited: SDK ACK remains within its 3-second window.
47
47
  },
48
48
  });
@@ -5,7 +5,11 @@ export interface MediaBinary {
5
5
  readonly fileName: string;
6
6
  }
7
7
  /** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
8
- export declare function resolveMediaBinary(media: MediaRef, download?: (url: string) => Promise<Buffer>): Promise<MediaBinary>;
8
+ export interface MediaResolveOptions {
9
+ readonly download?: (url: string, signal?: AbortSignal) => Promise<Buffer>;
10
+ readonly signal?: AbortSignal;
11
+ }
12
+ export declare function resolveMediaBinary(media: MediaRef, { download, signal }?: MediaResolveOptions): Promise<MediaBinary>;
9
13
  /** /im/v1/images 的 multipart body(image_type=message 用于消息内图片)。 */
10
14
  export declare function buildImageUploadForm(binary: MediaBinary): FormData;
11
15
  /**
@@ -13,8 +13,8 @@ const MIME_EXT = {
13
13
  'image/webp': 'webp',
14
14
  'image/bmp': 'bmp',
15
15
  };
16
- /** 从 canonical MediaRef 解析二进制:base64 解码 / 本地读盘 / URL 下载。 */
17
- export async function resolveMediaBinary(media, download = defaultDownload) {
16
+ export async function resolveMediaBinary(media, { download = defaultDownload, signal } = {}) {
17
+ signal?.throwIfAborted();
18
18
  const mimeType = media.mime_type ?? 'image/png';
19
19
  const ext = MIME_EXT[mimeType] ?? 'png';
20
20
  if (media.kind === 'base64') {
@@ -25,12 +25,12 @@ export async function resolveMediaBinary(media, download = defaultDownload) {
25
25
  }
26
26
  if (media.kind === 'path') {
27
27
  const path = media.value.startsWith('file://') ? media.value.slice('file://'.length) : media.value;
28
- return { data: await readFile(path), mimeType, fileName: basename(path) };
28
+ return { data: await readFile(path, { signal }), mimeType, fileName: basename(path) };
29
29
  }
30
- return { data: await download(media.value), mimeType, fileName: `image.${ext}` };
30
+ return { data: await download(media.value, signal), mimeType, fileName: `image.${ext}` };
31
31
  }
32
- async function defaultDownload(url) {
33
- const response = await fetch(url, { signal: AbortSignal.timeout(30_000) });
32
+ async function defaultDownload(url, signal) {
33
+ const response = await fetch(url, { signal: signal ? AbortSignal.any([signal, AbortSignal.timeout(30_000)]) : AbortSignal.timeout(30_000) });
34
34
  if (!response.ok)
35
35
  throw new Error(`download failed: HTTP ${response.status}`);
36
36
  return Buffer.from(await response.arrayBuffer());
package/lib/protocol.d.ts CHANGED
@@ -103,7 +103,7 @@ export declare function resolveChatType(chatId?: string, chatType?: string): 'gr
103
103
  * 不产生 `parent`。
104
104
  */
105
105
  export declare function larkInboundConversation(endpointKey: string, msg: LarkMessage): ConversationRef;
106
- export declare function resolveSender(msg: LarkMessage): string;
106
+ export declare function resolveSender(msg: LarkMessage): string | undefined;
107
107
  export declare function generateMessageId(msg: LarkMessage): string;
108
108
  /** Build inbound text for OutboundMessageService.receive. */
109
109
  export declare function formatInboundContent(msg: LarkMessage): string;
package/lib/protocol.js CHANGED
@@ -78,7 +78,7 @@ export function larkInboundConversation(endpointKey, msg) {
78
78
  export function resolveSender(msg) {
79
79
  return msg.sender?.sender_id?.open_id
80
80
  || msg.sender?.sender_id?.user_id
81
- || 'unknown';
81
+ || undefined;
82
82
  }
83
83
  export function generateMessageId(msg) {
84
84
  return msg.message_id || `${msg.create_time ?? Date.now()}`;
@@ -0,0 +1,4 @@
1
+ import type { HttpInstance } from '@larksuiteoapi/node-sdk';
2
+ import type { LarkFetch } from './client.js';
3
+ /** The installed SDK's documented HttpInstance seam, using the same account-owned transport. */
4
+ export declare function createLarkSdkHttpInstance(fetch: LarkFetch): HttpInstance;
@@ -0,0 +1,88 @@
1
+ import { Readable, PassThrough } from 'node:stream';
2
+ import { EndpointDeliveryError } from '@zhin.js/im-contract';
3
+ /** The installed SDK's documented HttpInstance seam, using the same account-owned transport. */
4
+ export function createLarkSdkHttpInstance(fetch) {
5
+ const request = async (input) => {
6
+ if (!input.url)
7
+ throw new TypeError('Lark SDK request URL is required');
8
+ const url = new URL(input.url);
9
+ if (input.params) {
10
+ const serializer = typeof input.paramsSerializer === 'function' ? input.paramsSerializer
11
+ : input.paramsSerializer?.serialize;
12
+ if (serializer) {
13
+ const query = serializer(input.params);
14
+ if (query)
15
+ url.search = url.search ? `${url.search}&${query}` : query;
16
+ }
17
+ else
18
+ for (const [key, value] of Object.entries(input.params)) {
19
+ if (value === undefined || value === null)
20
+ continue;
21
+ for (const item of Array.isArray(value) ? value : [value])
22
+ url.searchParams.append(key, String(item));
23
+ }
24
+ }
25
+ const headers = Object.fromEntries(Object.entries(input.headers ?? {}).filter(([, value]) => value !== undefined).map(([key, value]) => [key, String(value)]));
26
+ let body;
27
+ let streaming = false;
28
+ if (input.data !== undefined) {
29
+ const native = input.data;
30
+ if (typeof input.data === 'string' || input.data instanceof FormData || input.data instanceof ArrayBuffer || ArrayBuffer.isView(input.data))
31
+ body = input.data;
32
+ else if (native && (typeof native.pipe === 'function' || typeof native[Symbol.asyncIterator] === 'function')) {
33
+ if (native.getHeaders)
34
+ Object.assign(headers, native.getHeaders());
35
+ if (typeof native.pipe === 'function') {
36
+ const stream = new PassThrough();
37
+ stream.once('close', () => native.destroy?.());
38
+ native.pipe(stream);
39
+ body = stream;
40
+ }
41
+ else
42
+ body = input.data;
43
+ streaming = true;
44
+ }
45
+ else {
46
+ body = JSON.stringify(input.data);
47
+ if (!Object.keys(headers).some(key => key.toLowerCase() === 'content-type'))
48
+ headers['Content-Type'] = 'application/json';
49
+ }
50
+ }
51
+ const response = await fetch(url.toString(), { method: input.method, headers, body, ...(streaming ? { duplex: 'half' } : {}),
52
+ ...(input.timeout ? { signal: AbortSignal.timeout(input.timeout) } : {}) });
53
+ if (!response.ok)
54
+ throw new EndpointDeliveryError('http_error', `Lark HTTP ${response.status}`, response.status >= 400 && response.status < 500 && response.status !== 408 ? 'rejected' : 'unknown');
55
+ const native = response;
56
+ let data;
57
+ if (input.responseType === 'stream') {
58
+ if (!native.body)
59
+ throw new Error('Lark transport returned no response stream');
60
+ data = Readable.fromWeb(native.body);
61
+ }
62
+ else if (input.responseType === 'arraybuffer') {
63
+ if (!native.arrayBuffer)
64
+ throw new Error('Lark transport returned no binary reader');
65
+ data = Buffer.from(await native.arrayBuffer());
66
+ }
67
+ else if (input.responseType === 'blob') {
68
+ if (!native.blob)
69
+ throw new Error('Lark transport returned no blob reader');
70
+ data = await native.blob();
71
+ }
72
+ else if (input.responseType === 'text' || input.responseType === 'document')
73
+ data = await response.text();
74
+ else
75
+ data = await response.json();
76
+ return (input.$return_headers ? { data, headers: Object.fromEntries(native.headers?.entries() ?? []) } : data);
77
+ };
78
+ return {
79
+ request,
80
+ get: (url, options) => request({ ...options, url, method: 'GET' }),
81
+ delete: (url, options) => request({ ...options, url, method: 'DELETE' }),
82
+ head: (url, options) => request({ ...options, url, method: 'HEAD' }),
83
+ options: (url, options) => request({ ...options, url, method: 'OPTIONS' }),
84
+ post: (url, data, options) => request({ ...options, url, data, method: 'POST' }),
85
+ put: (url, data, options) => request({ ...options, url, data, method: 'PUT' }),
86
+ patch: (url, data, options) => request({ ...options, url, data, method: 'PATCH' }),
87
+ };
88
+ }
package/lib/webhook.d.ts CHANGED
@@ -9,7 +9,7 @@ export interface LarkWebhookHandler {
9
9
  readonly config: ResolvedLarkConfig;
10
10
  readonly isOpen: boolean;
11
11
  admitPlatform(name: string, event: unknown): void;
12
- admit(msg: LarkMessage): void;
12
+ admit(msg: LarkMessage, nativeEvent?: unknown): void;
13
13
  admitCardAction?(event: LarkCardAction): void;
14
14
  }
15
15
  export declare function registerLarkWebhookRoutes(http: HttpHost, handler: LarkWebhookHandler): HttpRouteRegistration[];
package/lib/webhook.js CHANGED
@@ -63,7 +63,7 @@ export async function handleLarkWebhookRequest(request, response, handler) {
63
63
  ...event.event.message,
64
64
  sender: event.event.message.sender ?? event.event.sender,
65
65
  };
66
- handler.admit(message);
66
+ handler.admit(message, event);
67
67
  }
68
68
  response.writeHead(200, { 'Content-Type': 'application/json' });
69
69
  response.end(JSON.stringify({ code: 0, msg: 'success' }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zhin.js/adapter-lark",
3
- "version": "1.1.18",
3
+ "version": "1.1.19",
4
4
  "description": "Zhin.js Lark/Feishu adapter (HTTP webhook and WebSocket long connection)",
5
5
  "type": "module",
6
6
  "main": "./lib/index.js",
@@ -35,27 +35,27 @@
35
35
  "dependencies": {
36
36
  "@larksuiteoapi/node-sdk": "^1.67.0",
37
37
  "undici": "6.29.1",
38
- "@zhin.js/adapter": "1.1.25",
39
- "@zhin.js/agent-feature": "1.1.10",
40
- "@zhin.js/client": "1.1.21",
38
+ "@zhin.js/adapter": "1.1.26",
39
+ "@zhin.js/agent-feature": "1.1.11",
40
+ "@zhin.js/client": "1.1.22",
41
41
  "@zhin.js/console-contract": "1.1.3",
42
- "@zhin.js/core": "1.1.53",
43
- "@zhin.js/feature-kit": "1.1.9",
44
- "@zhin.js/host-http": "1.1.11",
42
+ "@zhin.js/core": "1.1.54",
43
+ "@zhin.js/feature-kit": "1.1.10",
44
+ "@zhin.js/host-http": "1.1.12",
45
45
  "@zhin.js/im-contract": "1.1.9",
46
46
  "@zhin.js/logger": "1.1.2",
47
- "@zhin.js/page": "1.1.10",
48
- "@zhin.js/skill": "1.1.10",
49
- "@zhin.js/tool": "1.1.12"
47
+ "@zhin.js/page": "1.1.11",
48
+ "@zhin.js/skill": "1.1.11",
49
+ "@zhin.js/tool": "1.1.13"
50
50
  },
51
51
  "peerDependencies": {
52
52
  "zod": "^4.0.0",
53
- "@zhin.js/adapter": "^1.1.25",
54
- "@zhin.js/command": "^1.1.11",
55
- "@zhin.js/core": "^1.1.53",
56
- "@zhin.js/host-http": "^1.1.11",
57
- "@zhin.js/permission": "^1.1.9",
58
- "zhin.js": "^1.1.18"
53
+ "@zhin.js/adapter": "^1.1.26",
54
+ "@zhin.js/command": "^1.1.12",
55
+ "@zhin.js/core": "^1.1.54",
56
+ "@zhin.js/host-http": "^1.1.12",
57
+ "@zhin.js/permission": "^1.1.10",
58
+ "zhin.js": "^1.1.19"
59
59
  },
60
60
  "peerDependenciesMeta": {
61
61
  "@zhin.js/command": {
@@ -73,7 +73,7 @@
73
73
  "typescript": "^6.0.3",
74
74
  "vitest": "^4.1.10",
75
75
  "zod": "^4.4.3",
76
- "zhin.js": "1.1.18"
76
+ "zhin.js": "1.1.19"
77
77
  },
78
78
  "files": [
79
79
  "adapters",
@@ -105,7 +105,7 @@
105
105
  "features": [
106
106
  {
107
107
  "package": "@zhin.js/adapter",
108
- "api": "^1.0.0"
108
+ "api": "^2.0.0"
109
109
  },
110
110
  {
111
111
  "package": "@zhin.js/command",
package/plugin.js CHANGED
@@ -1,9 +1,7 @@
1
1
  // Generated by build-plugin-runtime-entries.mjs. Do not edit.
2
- import { createEndpointRuntimeState } from 'zhin.js/adapter';
3
2
  import { definePlugin, pluginConsoleRpcToken } from 'zhin.js';
4
3
  import { permissionHostToken } from '@zhin.js/permission';
5
4
  import { checkLarkPlatformPermit } from "./lib/platform-permit.js";
6
- import { larkRuntimeStateToken } from "./lib/lark-runtime-state.js";
7
5
  import { createLarkConsoleRpc } from "./lib/console.js";
8
6
  export default definePlugin({
9
7
  name: 'lark',
@@ -11,7 +9,6 @@ export default definePlugin({
11
9
  displayName: 'Lark/Feishu (飞书) Adapter',
12
10
  },
13
11
  setup(context) {
14
- context.resources.provide(larkRuntimeStateToken, createEndpointRuntimeState());
15
12
  context.resources.provide(pluginConsoleRpcToken, createLarkConsoleRpc(context.resources, () => context.config.get()));
16
13
  if (context.resources.has(permissionHostToken)) {
17
14
  const host = context.resources.use(permissionHostToken);
package/src/adapter.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { LarkClient, type LarkClientEventMap } from './client.js';
2
+ import { Adapter, type AdapterContext, type EndpointCreationContext } from 'zhin.js/adapter';
3
+ import { LarkEndpoint } from './endpoint.js';
4
+ import { resolveLarkConfig, type LarkEndpointConfig } from './protocol.js';
5
+ import { httpHostToken } from '@zhin.js/host-http';
6
+
7
+ export class LarkAdapter extends Adapter<LarkEndpoint, LarkEndpointConfig, LarkClientEventMap> {
8
+ readonly #context: AdapterContext<LarkEndpointConfig>;
9
+ constructor(context: AdapterContext<LarkEndpointConfig>) { super(); this.#context = context; }
10
+ override get configurationIdentityFields(): readonly string[] { return ['id', 'appId', 'isFeishu']; }
11
+ override get configurationUpdateFields(): readonly string[] { return ['appSecret', 'encryptKey', 'verificationToken', 'mode', 'webhookPath', 'apiBaseUrl', 'webApiProxy', 'streamProxy']; }
12
+ override get configurationIdentityProbeFields(): readonly string[] { return ['appSecret', 'apiBaseUrl']; }
13
+ protected override async identifyConfiguration(input: LarkEndpointConfig, signal: AbortSignal, _changed: readonly string[]): Promise<string | undefined> {
14
+ const config = resolveLarkConfig(input), client = new LarkClient({ id: this.descriptor.id, config, signal });
15
+ try {
16
+ const response = await client.request<{ code?: number; bot?: { open_id?: unknown } }>({ url: `${config.apiBaseUrl}/bot/v3/info`, method: 'GET' });
17
+ if (response.code !== 0 || typeof response.bot?.open_id !== 'string' || !response.bot.open_id) throw new Error('Lark identity verification returned no bot open_id');
18
+ return JSON.stringify(['lark', new URL(config.apiBaseUrl).origin, response.bot.open_id]);
19
+ } finally { await client[Symbol.asyncDispose](); }
20
+ }
21
+ protected override validateConfiguration(config: LarkEndpointConfig): void { resolveLarkConfig(config); }
22
+ protected createEndpoint(input: LarkEndpointConfig, execution: EndpointCreationContext): LarkEndpoint {
23
+ const config = resolveLarkConfig(input);
24
+ return new LarkEndpoint({ id: execution.canonicalId, config, ...(config.mode === 'websocket' ? {} : { http: this.#context.use(httpHostToken) }) });
25
+ }
26
+ }
27
+
28
+ declare module '@zhin.js/feature-kit' {
29
+ interface AdapterRegistry { readonly 'lark': LarkAdapter; }
30
+ }