@zhin.js/adapter-lark 1.1.18 → 1.1.20
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 +45 -0
- package/README.md +8 -7
- package/adapters/lark/index.js +3 -17
- package/adapters/lark/index.ts +2 -17
- package/commands/lark/endpoint/definition.js +0 -2
- package/commands/lark/endpoint/definition.ts +0 -2
- package/lib/adapter.d.ts +19 -0
- package/lib/adapter.js +29 -0
- package/lib/client.d.ts +95 -11
- package/lib/client.js +657 -2
- package/lib/console.d.ts +1 -1
- package/lib/console.js +2 -5
- package/lib/endpoint.d.ts +11 -102
- package/lib/endpoint.js +26 -671
- package/lib/index.d.ts +4 -2
- package/lib/index.js +3 -2
- package/lib/long-connection.d.ts +1 -1
- package/lib/long-connection.js +1 -1
- package/lib/media-upload.d.ts +5 -1
- package/lib/media-upload.js +6 -6
- package/lib/protocol.d.ts +1 -1
- package/lib/protocol.js +1 -1
- package/lib/sdk-http.d.ts +4 -0
- package/lib/sdk-http.js +88 -0
- package/lib/webhook.d.ts +1 -1
- package/lib/webhook.js +1 -1
- package/package.json +18 -18
- package/plugin.js +0 -3
- package/src/adapter.ts +30 -0
- package/src/client.ts +726 -10
- package/src/console.ts +2 -5
- package/src/endpoint.ts +29 -749
- package/src/index.ts +4 -3
- package/src/long-connection.ts +2 -2
- package/src/media-upload.ts +10 -5
- package/src/protocol.ts +2 -2
- package/src/sdk-http.ts +65 -0
- package/src/webhook.ts +2 -2
- package/lib/lark-runtime-state.d.ts +0 -1
- package/lib/lark-runtime-state.js +0 -6
- package/src/lark-runtime-state.ts +0 -7
package/src/endpoint.ts
CHANGED
|
@@ -1,756 +1,36 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import type
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
type EndpointSendRequest,
|
|
19
|
-
} from 'zhin.js/adapter';
|
|
20
|
-
import type { HttpHost, HttpRouteRegistration } from '@zhin.js/host-http';
|
|
21
|
-
import { formatCompact, getAdapterLogger } from '@zhin.js/logger';
|
|
22
|
-
import type { CapabilityId } from 'zhin.js';
|
|
23
|
-
import {
|
|
24
|
-
buildImageUploadForm,
|
|
25
|
-
readOutboundImageMedia,
|
|
26
|
-
resolveMediaBinary,
|
|
27
|
-
} from './media-upload.js';
|
|
28
|
-
import {
|
|
29
|
-
formatInboundContent,
|
|
30
|
-
formatOutboundBody,
|
|
31
|
-
generateMessageId,
|
|
32
|
-
larkInboundConversation,
|
|
33
|
-
resolveChatType,
|
|
34
|
-
resolveSender,
|
|
35
|
-
type AccessToken,
|
|
36
|
-
type LarkApiResponse,
|
|
37
|
-
type LarkMessage,
|
|
38
|
-
type ResolvedLarkConfig,
|
|
39
|
-
} from './protocol.js';
|
|
40
|
-
import { normalizeLarkSenderForPermit } from './platform-permit.js';
|
|
41
|
-
import { registerLarkWebhookRoutes } from './webhook.js';
|
|
42
|
-
import { createLarkLongConnection, type LarkLongConnectionFactory } from './long-connection.js';
|
|
43
|
-
|
|
44
|
-
/** 出站 HTTP 调用统一 30s 超时。 */
|
|
45
|
-
const OUTBOUND_TIMEOUT_MS = 30_000;
|
|
46
|
-
|
|
47
|
-
export type LarkFetch = (
|
|
48
|
-
url: string,
|
|
49
|
-
init?: {
|
|
50
|
-
readonly method?: string;
|
|
51
|
-
readonly headers?: Record<string, string>;
|
|
52
|
-
readonly body?: string | FormData;
|
|
53
|
-
readonly signal?: AbortSignal;
|
|
54
|
-
},
|
|
55
|
-
) => Promise<{
|
|
56
|
-
readonly ok: boolean;
|
|
57
|
-
readonly status: number;
|
|
58
|
-
text(): Promise<string>;
|
|
59
|
-
json(): Promise<unknown>;
|
|
60
|
-
}>;
|
|
61
|
-
|
|
62
|
-
export interface LarkEndpointOptions {
|
|
63
|
-
readonly id: CapabilityId;
|
|
64
|
-
readonly http?: HttpHost;
|
|
65
|
-
readonly longConnectionFactory?: LarkLongConnectionFactory;
|
|
66
|
-
readonly config: ResolvedLarkConfig;
|
|
67
|
-
readonly fetch?: LarkFetch;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export interface LarkClientApi {
|
|
71
|
-
getUserInfo(userId: string, userIdType?: 'open_id' | 'user_id' | 'union_id'): Promise<unknown>;
|
|
72
|
-
getChatInfo(chatId: string): Promise<unknown>;
|
|
73
|
-
uploadFile(filePath: string, fileType?: 'image' | 'file' | 'video' | 'audio'): Promise<string | null>;
|
|
74
|
-
createChat(name: string, userIds: string[], ownerId?: string): Promise<string | null>;
|
|
75
|
-
updateChatInfo(chatId: string, options: { name?: string; description?: string }): Promise<boolean>;
|
|
76
|
-
addChatMembers(chatId: string, userIds: string[]): Promise<boolean>;
|
|
77
|
-
removeChatMembers(chatId: string, userIds: string[]): Promise<boolean>;
|
|
78
|
-
getChatMembers(chatId: string): Promise<unknown[]>;
|
|
79
|
-
dissolveChat(chatId: string): Promise<boolean>;
|
|
80
|
-
setChatManagers(chatId: string, userIds: string[]): Promise<boolean>;
|
|
81
|
-
removeChatManagers(chatId: string, userIds: string[]): Promise<boolean>;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
/** Feishu/Lark OpenAPI client surface carried by every Endpoint event. */
|
|
85
|
-
export class LarkClient implements LarkClientApi {
|
|
86
|
-
constructor(readonly api: LarkClientApi) {}
|
|
87
|
-
getUserInfo = (id: string, type?: 'open_id' | 'user_id' | 'union_id') =>
|
|
88
|
-
this.api.getUserInfo(id, type);
|
|
89
|
-
getChatInfo = (id: string) => this.api.getChatInfo(id);
|
|
90
|
-
uploadFile = (path: string, type?: 'image' | 'file' | 'video' | 'audio') =>
|
|
91
|
-
this.api.uploadFile(path, type);
|
|
92
|
-
createChat = (name: string, users: string[], owner?: string) =>
|
|
93
|
-
this.api.createChat(name, users, owner);
|
|
94
|
-
updateChatInfo = (id: string, options: { name?: string; description?: string }) =>
|
|
95
|
-
this.api.updateChatInfo(id, options);
|
|
96
|
-
addChatMembers = (id: string, users: string[]) => this.api.addChatMembers(id, users);
|
|
97
|
-
removeChatMembers = (id: string, users: string[]) => this.api.removeChatMembers(id, users);
|
|
98
|
-
getChatMembers = (id: string) => this.api.getChatMembers(id);
|
|
99
|
-
dissolveChat = (id: string) => this.api.dissolveChat(id);
|
|
100
|
-
setChatManagers = (id: string, users: string[]) => this.api.setChatManagers(id, users);
|
|
101
|
-
removeChatManagers = (id: string, users: string[]) => this.api.removeChatManagers(id, users);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const LARK_CHAT_MANAGER_CACHE_TTL_MS = 60_000;
|
|
105
|
-
|
|
106
|
-
export class LarkEndpoint extends Endpoint<LarkClient> {
|
|
107
|
-
readonly client = new LarkClient({
|
|
108
|
-
getUserInfo: (id, type) => this.getUserInfo(id, type),
|
|
109
|
-
getChatInfo: (id) => this.getChatInfo(id),
|
|
110
|
-
uploadFile: (path, type) => this.uploadFile(path, type),
|
|
111
|
-
createChat: (name, users, owner) => this.createChat(name, users, owner),
|
|
112
|
-
updateChatInfo: (id, options) => this.updateChatInfo(id, options),
|
|
113
|
-
addChatMembers: (id, users) => this.addChatMembers(id, users),
|
|
114
|
-
removeChatMembers: (id, users) => this.removeChatMembers(id, users),
|
|
115
|
-
getChatMembers: (id) => this.getChatMembers(id),
|
|
116
|
-
dissolveChat: (id) => this.dissolveChat(id),
|
|
117
|
-
setChatManagers: (id, users) => this.setChatManagers(id, users),
|
|
118
|
-
removeChatManagers: (id, users) => this.removeChatManagers(id, users),
|
|
119
|
-
});
|
|
120
|
-
readonly #logger!: ReturnType<typeof getAdapterLogger>;
|
|
121
|
-
|
|
122
|
-
readonly #options: LarkEndpointOptions;
|
|
123
|
-
readonly #fetch: LarkFetch;
|
|
124
|
-
readonly #webApiTransport?: ReturnType<typeof createLarkWebApiTransport>;
|
|
125
|
-
/**
|
|
126
|
-
* Console 社交面语义端口。Lark OpenAPI 仅接读取类列表能力:
|
|
127
|
-
* - listGroups:GET /im/v1/chats(bot 所在群列表,分页归一为 {group_id: chat_id, name})
|
|
128
|
-
* - listGroupMembers:GET /im/v1/chats/:chat_id/members(open_id 分页,返回平台原生成员形状)
|
|
129
|
-
* 不接的项:listFriends(Lark 无好友概念,contact API 为组织通讯录而非社交关系)、
|
|
130
|
-
* listChannels(无频道/guild 概念)、写操作(审批/踢人/禁言等本轮不在范围内)。
|
|
131
|
-
*/
|
|
132
|
-
readonly management: EndpointManagement = Object.freeze<EndpointManagement>({
|
|
133
|
-
listGroups: async (): Promise<readonly EndpointGroup[]> => {
|
|
134
|
-
const items = await this.#listAllPages('/im/v1/chats');
|
|
135
|
-
const groups: EndpointGroup[] = [];
|
|
136
|
-
for (const item of items) {
|
|
137
|
-
const chatId = typeof item.chat_id === 'string' ? item.chat_id : '';
|
|
138
|
-
if (!chatId) continue;
|
|
139
|
-
groups.push({
|
|
140
|
-
group_id: chatId,
|
|
141
|
-
name: typeof item.name === 'string' && item.name ? item.name : chatId,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
return groups;
|
|
145
|
-
},
|
|
146
|
-
listGroupMembers: (groupId) => this.#listAllPages(
|
|
147
|
-
`/im/v1/chats/${encodeURIComponent(groupId)}/members`,
|
|
148
|
-
{ member_id_type: 'open_id' },
|
|
149
|
-
),
|
|
150
|
-
});
|
|
151
|
-
readonly control: EndpointControl = createRecallEndpointControl((id) => this.recallMessage(id));
|
|
152
|
-
#routeReleases: HttpRouteRegistration[] = [];
|
|
153
|
-
#accessToken: AccessToken = { token: '', expires_in: 0, timestamp: 0 };
|
|
154
|
-
#refreshPromise: Promise<string> | null = null;
|
|
155
|
-
#open = false;
|
|
156
|
-
#started = false;
|
|
157
|
-
readonly #lifecycle: EndpointLifecycle;
|
|
158
|
-
readonly #cardSources = new Map<string, { conversation: ConversationRef; time: number }>();
|
|
159
|
-
readonly #seen = new Map<string, number>();
|
|
160
|
-
readonly #chatManagers = new Map<string, { at: number; owner?: string; admins: ReadonlySet<string> }>();
|
|
161
|
-
/** One chat.get per chat at a time: concurrent messages on a cache miss share the lookup. */
|
|
162
|
-
readonly #chatManagerLookups = new Map<string, Promise<{ at: number; owner?: string; admins: ReadonlySet<string> }>>();
|
|
163
|
-
#startupGeneration = 0;
|
|
164
|
-
readonly #pendingInbound: LarkMessage[] = [];
|
|
165
|
-
|
|
166
|
-
constructor(options: LarkEndpointOptions) {
|
|
167
|
-
super();
|
|
168
|
-
this.#logger = getAdapterLogger('lark', options.config.id);
|
|
169
|
-
this.#options = options;
|
|
170
|
-
if (options.config.webApiProxy && options.fetch) throw new TypeError('Lark webApiProxy cannot be combined with an injected fetch');
|
|
171
|
-
this.#webApiTransport = options.config.webApiProxy ? createLarkWebApiTransport(options.config.webApiProxy) : undefined;
|
|
172
|
-
this.#fetch = this.#webApiTransport?.fetch ?? options.fetch ?? globalThis.fetch;
|
|
173
|
-
this.#lifecycle = createEndpointLifecycle({ name: options.config.id });
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
/** Used by webhook handler. */
|
|
177
|
-
get isOpen(): boolean {
|
|
178
|
-
return this.#open;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
get config(): ResolvedLarkConfig {
|
|
182
|
-
return this.#options.config;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
get transportState(): EndpointTransportState | undefined {
|
|
186
|
-
return this.config.mode === 'websocket' ? this.#lifecycle.state : undefined;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
async start(): Promise<void> {
|
|
190
|
-
if (this.#started) return;
|
|
191
|
-
this.#webApiTransport?.reopen();
|
|
192
|
-
this.#started = true;
|
|
193
|
-
const generation = ++this.#startupGeneration;
|
|
194
|
-
try {
|
|
195
|
-
if (this.config.mode === 'websocket') {
|
|
196
|
-
await this.#lifecycle.start(async (handle) => {
|
|
197
|
-
let active = true;
|
|
198
|
-
const transport = (this.#options.longConnectionFactory ?? createLarkLongConnection)({
|
|
199
|
-
config: this.config,
|
|
200
|
-
receive: (message) => {
|
|
201
|
-
if (!active || !this.#started || generation !== this.#startupGeneration) return;
|
|
202
|
-
if (this.#open) this.admit(message);
|
|
203
|
-
else if (this.#pendingInbound.length < 100) this.#pendingInbound.push(message);
|
|
204
|
-
else this.#logger.warn('lark_inbound_startup_buffer_full');
|
|
205
|
-
},
|
|
206
|
-
cardAction: (event) => { if (active && this.#open && generation === this.#startupGeneration) this.admitCardAction(event); },
|
|
207
|
-
disconnected: () => { if (!active) return; active = false; transport.close(); handle.notifyClosed(); },
|
|
208
|
-
});
|
|
209
|
-
handle.onForceClose(() => { active = false; transport.close(); });
|
|
210
|
-
await transport.connect();
|
|
211
|
-
});
|
|
212
|
-
return;
|
|
213
|
-
}
|
|
214
|
-
if (!this.#options.config.encryptKey && !this.#options.config.verificationToken) {
|
|
215
|
-
// encryptKey / verificationToken 都未配置时 webhook 完全无鉴权,任何人可伪造事件
|
|
216
|
-
this.#logger.warn(formatCompact({
|
|
217
|
-
endpoint: this.#options.config.id,
|
|
218
|
-
op: 'webhook',
|
|
219
|
-
ok: false,
|
|
220
|
-
error: 'neither encryptKey nor verificationToken configured; webhook is unauthenticated',
|
|
221
|
-
}));
|
|
222
|
-
}
|
|
223
|
-
await this.#refreshAccessToken();
|
|
224
|
-
if (!this.#started || generation !== this.#startupGeneration) return;
|
|
225
|
-
if (!this.#options.http) throw new Error('Lark webhook mode requires HttpHost');
|
|
226
|
-
this.#routeReleases.push(...registerLarkWebhookRoutes(this.#options.http, this));
|
|
227
|
-
this.#logger.debug(formatCompact({
|
|
228
|
-
endpoint: this.#options.config.id,
|
|
229
|
-
op: 'webhook',
|
|
230
|
-
path: this.#options.config.webhookPath,
|
|
231
|
-
}));
|
|
232
|
-
} catch (error) {
|
|
233
|
-
await this.stop();
|
|
234
|
-
this.#logger.error('Failed to connect Lark endpoint:', error);
|
|
235
|
-
throw error;
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
open(): void {
|
|
240
|
-
this.#open = true;
|
|
241
|
-
for (const message of this.#pendingInbound.splice(0)) this.admit(message);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
close(): void {
|
|
245
|
-
this.#open = false;
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
async stop(): Promise<void> {
|
|
249
|
-
this.#open = false;
|
|
250
|
-
this.#pendingInbound.length = 0;
|
|
251
|
-
this.#cardSources.clear();
|
|
252
|
-
this.#started = false;
|
|
253
|
-
++this.#startupGeneration;
|
|
254
|
-
await this.#lifecycle.stop();
|
|
255
|
-
await this.#webApiTransport?.close();
|
|
256
|
-
for (const release of this.#routeReleases.splice(0)) release();
|
|
257
|
-
this.#started = false;
|
|
258
|
-
this.#logger.debug(formatCompact({ op: 'disconnect' }));
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
async send({ conversation, payload, locale }: EndpointSendRequest): Promise<string> {
|
|
262
|
-
try {
|
|
263
|
-
const materialized = await this.#materializeOutboundMedia(payload);
|
|
264
|
-
const content = formatOutboundBody(materialized, locale);
|
|
265
|
-
const data = await this.#request(content.replyTo
|
|
266
|
-
? `/im/v1/messages/${encodeURIComponent(content.replyTo)}/reply`
|
|
267
|
-
: '/im/v1/messages', {
|
|
268
|
-
method: 'POST',
|
|
269
|
-
...(content.replyTo ? {} : { params: { receive_id_type: 'chat_id' } }),
|
|
270
|
-
body: {
|
|
271
|
-
...(content.replyTo ? {} : { receive_id: conversation.id }),
|
|
272
|
-
msg_type: content.msg_type,
|
|
273
|
-
content: content.content,
|
|
274
|
-
},
|
|
275
|
-
});
|
|
276
|
-
if (typeof data.code === 'number' && data.code !== 0) {
|
|
277
|
-
throw new EndpointDeliveryError('platform_rejected', `Lark rejected message (code=${data.code})`, 'rejected');
|
|
278
|
-
}
|
|
279
|
-
if (data.code !== 0) throw new EndpointDeliveryError('delivery_unconfirmed', 'Malformed platform response', 'unknown');
|
|
280
|
-
const messageId = requireMessageId(data.data?.message_id);
|
|
281
|
-
if (content.msg_type === 'interactive') {
|
|
282
|
-
const now = Date.now();
|
|
283
|
-
for (const [id, source] of this.#cardSources) if (now - source.time > 300_000) this.#cardSources.delete(id);
|
|
284
|
-
this.#cardSources.set(messageId, { conversation, time: now });
|
|
285
|
-
if (this.#cardSources.size > 10_000) this.#cardSources.delete(this.#cardSources.keys().next().value!);
|
|
286
|
-
}
|
|
287
|
-
this.#logger.debug(formatCompact({ op: 'send', to: conversation.id,
|
|
288
|
-
id: messageId,
|
|
289
|
-
}));
|
|
290
|
-
return messageId;
|
|
291
|
-
} catch (error) {
|
|
292
|
-
this.#logger.warn(formatCompact({
|
|
293
|
-
op: 'lark_send_failed',
|
|
294
|
-
status: error instanceof LarkRequestError ? error.status : undefined,
|
|
295
|
-
platform_code: error instanceof LarkRequestError ? error.platformCode : undefined,
|
|
296
|
-
outcome: error instanceof EndpointDeliveryError ? error.disposition : 'unknown',
|
|
297
|
-
}));
|
|
298
|
-
if (error instanceof EndpointDeliveryError) throw error;
|
|
299
|
-
throw new EndpointDeliveryError('delivery_unconfirmed', 'Outbound request outcome is unknown', 'unknown', { cause: error });
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
async recallMessage(messageId: string): Promise<void> {
|
|
304
|
-
if (!messageId || messageId.startsWith('outbound:')) return;
|
|
305
|
-
await this.#request(`/im/v1/messages/${encodeURIComponent(messageId)}`, { method: 'DELETE' });
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
/**
|
|
309
|
-
* im/v1 消息 image 段只接受 image_key:canonical MediaRef(base64/本地路径/URL)
|
|
310
|
-
* 先经 /im/v1/images 物化,产物回写为 MediaRef kind=file(平台不透明引用);
|
|
311
|
-
* 上传失败阻止消息发送并保留拒绝或未知分类。
|
|
312
|
-
*/
|
|
313
|
-
async #materializeOutboundMedia(payload: unknown): Promise<unknown> {
|
|
314
|
-
if (!Array.isArray(payload)) return payload;
|
|
315
|
-
return Promise.all(payload.map(async (item) => {
|
|
316
|
-
if (typeof item === 'string' || !item || typeof item !== 'object') return item;
|
|
317
|
-
const seg = item as { type?: unknown; data?: Record<string, unknown> };
|
|
318
|
-
if (seg.type !== 'image') return item;
|
|
319
|
-
const data = seg.data ?? {};
|
|
320
|
-
const media = readOutboundImageMedia(data);
|
|
321
|
-
if (!media) return item;
|
|
322
|
-
const imageKey = await this.uploadImage(media);
|
|
323
|
-
if (!imageKey) throw new EndpointDeliveryError('delivery_unconfirmed', 'Lark image upload returned no image key', 'unknown');
|
|
324
|
-
return {
|
|
325
|
-
type: 'image',
|
|
326
|
-
data: { media: { kind: 'file', value: imageKey } },
|
|
327
|
-
};
|
|
328
|
-
}));
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
/** Upload must succeed before issuing a message; dropped media is never confirmed. */
|
|
332
|
-
async uploadImage(media: Parameters<typeof resolveMediaBinary>[0]): Promise<string> {
|
|
333
|
-
await this.#ensureAccessToken();
|
|
334
|
-
const binary = await resolveMediaBinary(media);
|
|
335
|
-
const form = buildImageUploadForm(binary);
|
|
336
|
-
const response = await this.#fetch(`${this.#options.config.apiBaseUrl}/im/v1/images`, {
|
|
337
|
-
method: 'POST',
|
|
338
|
-
headers: { Authorization: `Bearer ${this.#accessToken.token}` },
|
|
339
|
-
body: form,
|
|
340
|
-
signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
|
|
341
|
-
});
|
|
342
|
-
const data = await response.json().catch(() => undefined) as LarkApiResponse | undefined;
|
|
343
|
-
const code = typeof data?.code === 'number' && Number.isFinite(data.code) ? data.code : undefined;
|
|
344
|
-
if (!response.ok) throw new LarkRequestError(response.status, code, response.status >= 400 && response.status < 500 && response.status !== 408);
|
|
345
|
-
if (code !== 0) throw new LarkRequestError(response.status, code, code !== undefined);
|
|
346
|
-
const key = data?.data?.image_key;
|
|
347
|
-
if (typeof key !== 'string' || !key.trim()) throw new LarkRequestError(response.status, code, false);
|
|
348
|
-
return key;
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
/** Test / internal: admit a parsed message when open (non-webhook path). */
|
|
352
|
-
admit(msg: LarkMessage): void {
|
|
353
|
-
if (!this.#open) return;
|
|
354
|
-
if (msg.message_id) {
|
|
355
|
-
const now = Date.now();
|
|
356
|
-
for (const [id, time] of this.#seen) if (now - time > 300_000) this.#seen.delete(id);
|
|
357
|
-
if (this.#seen.has(msg.message_id)) return;
|
|
358
|
-
this.#seen.set(msg.message_id, now);
|
|
359
|
-
if (this.#seen.size > 10_000) this.#seen.delete(this.#seen.keys().next().value!);
|
|
360
|
-
}
|
|
361
|
-
const conversation = larkInboundConversation(String(this.#options.id), msg);
|
|
362
|
-
void this.#emitInbound(msg, conversation).catch((err) => {
|
|
363
|
-
this.#logger.warn(formatCompact({
|
|
364
|
-
op: 'lark_gateway_receive_failed',
|
|
365
|
-
conversation: conversation.id,
|
|
366
|
-
error: err instanceof Error ? err.message : String(err),
|
|
367
|
-
}));
|
|
368
|
-
});
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
async #emitInbound(msg: LarkMessage, conversation: ConversationRef): Promise<void> {
|
|
372
|
-
const chatType = resolveChatType(msg.chat_id, msg.chat_type);
|
|
373
|
-
const senderId = resolveSender(msg);
|
|
374
|
-
const permit = chatType === 'group' && msg.chat_id
|
|
375
|
-
? normalizeLarkSenderForPermit(await this.#resolveChatRole(msg.chat_id, senderId))
|
|
376
|
-
: undefined;
|
|
377
|
-
// 规范角色 owner / admin 进 roles;细粒度能力放 senderPermissions
|
|
378
|
-
const role = permit?.role === 'owner' || permit?.role === 'admin' ? permit.role : undefined;
|
|
379
|
-
await this.emit('message.receive', {
|
|
380
|
-
conversation,
|
|
381
|
-
message: { conversation, id: generateMessageId(msg) },
|
|
382
|
-
content: formatInboundContent(msg),
|
|
383
|
-
sender: { id: senderId, ...(role ? { roles: [role] } : {}) },
|
|
384
|
-
endpointId: this.#options.config.id,
|
|
385
|
-
metadata: Object.freeze({
|
|
386
|
-
messageType: msg.message_type,
|
|
387
|
-
chatType,
|
|
388
|
-
...(role ? { senderRole: role } : {}),
|
|
389
|
-
...(role && permit?.permissions?.length ? { senderPermissions: [...permit.permissions] } : {}),
|
|
390
|
-
}),
|
|
391
|
-
});
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
/** 群主 / 群管理员来自 chat.get(owner_id + 管理员列表),按群缓存;失败时保守地视为普通成员。 */
|
|
395
|
-
async #resolveChatRole(chatId: string, senderId: string): Promise<{ isOwner: boolean; isAdmin: boolean }> {
|
|
396
|
-
const now = Date.now();
|
|
397
|
-
for (const [id, entry] of this.#chatManagers) if (now - entry.at > LARK_CHAT_MANAGER_CACHE_TTL_MS) this.#chatManagers.delete(id);
|
|
398
|
-
let entry = this.#chatManagers.get(chatId);
|
|
399
|
-
if (!entry) {
|
|
400
|
-
let lookup = this.#chatManagerLookups.get(chatId);
|
|
401
|
-
if (!lookup) {
|
|
402
|
-
lookup = (async () => {
|
|
403
|
-
const info = await this.getChatInfo(chatId) as {
|
|
404
|
-
owner_id?: unknown;
|
|
405
|
-
user_manager_id_list?: unknown;
|
|
406
|
-
} | null;
|
|
407
|
-
const admins = Array.isArray(info?.user_manager_id_list) ? info.user_manager_id_list.map(String) : [];
|
|
408
|
-
const fetched = {
|
|
409
|
-
at: Date.now(),
|
|
410
|
-
...(typeof info?.owner_id === 'string' && info.owner_id ? { owner: info.owner_id } : {}),
|
|
411
|
-
admins: new Set(admins) as ReadonlySet<string>,
|
|
412
|
-
};
|
|
413
|
-
this.#chatManagers.set(chatId, fetched);
|
|
414
|
-
return fetched;
|
|
415
|
-
})().finally(() => this.#chatManagerLookups.delete(chatId));
|
|
416
|
-
this.#chatManagerLookups.set(chatId, lookup);
|
|
417
|
-
}
|
|
418
|
-
entry = await lookup;
|
|
419
|
-
}
|
|
420
|
-
return {
|
|
421
|
-
isOwner: entry.owner !== undefined && entry.owner === senderId,
|
|
422
|
-
isAdmin: entry.admins.has(senderId),
|
|
1
|
+
import { createEndpointSpecification, endpointSpec, type Endpoint } from 'zhin.js/adapter';
|
|
2
|
+
import { LarkClient, type LarkEndpointOptions } from './client.js';
|
|
3
|
+
export type { LarkEndpointOptions } from './client.js';
|
|
4
|
+
export type { LarkFetch } from './client.js';
|
|
5
|
+
|
|
6
|
+
/** Actual protocol Client; framework lifecycle and admission stay in their symbol namespace. */
|
|
7
|
+
export class LarkEndpoint extends LarkClient implements Endpoint {
|
|
8
|
+
readonly [endpointSpec] = this.#createSpecification();
|
|
9
|
+
constructor(options: LarkEndpointOptions) { super(options); }
|
|
10
|
+
#createSpecification() {
|
|
11
|
+
const hooks = {
|
|
12
|
+
start: (signal: AbortSignal) => this.#startFramework(signal),
|
|
13
|
+
open: () => this.openInput(), close: () => this.closeInput(), stop: () => this.disconnectTransport(),
|
|
14
|
+
send: (request: import('zhin.js/adapter').EndpointSendRequest) => this.sendPayload(request),
|
|
15
|
+
name: this.configName(),
|
|
16
|
+
control: this.endpointControl,
|
|
17
|
+
management: this.endpointManagement,
|
|
423
18
|
};
|
|
19
|
+
Object.defineProperty(hooks, 'transportState', { get: () => this.transportState });
|
|
20
|
+
return createEndpointSpecification(this, hooks);
|
|
424
21
|
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
if (!this.#open) return;
|
|
429
|
-
const sourceId = event.context?.open_message_id ?? event.open_message_id;
|
|
430
|
-
const chatId = event.context?.open_chat_id ?? event.open_chat_id;
|
|
431
|
-
const sender = event.operator?.open_id ?? event.operator?.user_id;
|
|
432
|
-
const payload = event.action?.value?.zhin_payload;
|
|
433
|
-
if (!sourceId || !chatId || !sender || typeof payload !== 'string' || event.action?.tag !== 'button') return;
|
|
434
|
-
const source = this.#cardSources.get(sourceId);
|
|
435
|
-
if (!source || source.conversation.id !== chatId || Date.now() - source.time > 300_000) return;
|
|
436
|
-
const dedup = `card:${event.token ?? `${sourceId}:${sender}:${payload}`}`;
|
|
437
|
-
if (this.#seen.has(dedup)) return;
|
|
438
|
-
this.#seen.set(dedup, Date.now());
|
|
439
|
-
if (this.#seen.size > 10_000) this.#seen.delete(this.#seen.keys().next().value!);
|
|
440
|
-
const conversation = source.conversation;
|
|
441
|
-
void this.emit('message.receive', {
|
|
442
|
-
conversation, message: { conversation, id: dedup }, content: '',
|
|
443
|
-
segments: [{ type: 'action', data: { payload } }], sender: { id: sender }, endpointId: this.config.id,
|
|
444
|
-
metadata: Object.freeze({ eventType: 'card.action.trigger', sourceMessageId: sourceId }),
|
|
445
|
-
}).catch(() => this.#logger.warn('lark_card_action_receive_failed'));
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
admitPlatform(name: string, event: unknown): void {
|
|
449
|
-
if (!this.#open) return;
|
|
450
|
-
void this.emitPlatform(name, event).catch((error) => {
|
|
451
|
-
this.#logger.warn(formatCompact({
|
|
452
|
-
op: 'lark_platform_event_failed',
|
|
453
|
-
event: name,
|
|
454
|
-
error: error instanceof Error ? error.message : String(error),
|
|
455
|
-
}));
|
|
456
|
-
});
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
async getUserInfo(
|
|
460
|
-
userId: string,
|
|
461
|
-
userIdType: 'open_id' | 'user_id' | 'union_id' = 'open_id',
|
|
462
|
-
): Promise<unknown> {
|
|
463
|
-
try {
|
|
464
|
-
const data = await this.#request(`/contact/v3/users/${userId}`, {
|
|
465
|
-
method: 'GET',
|
|
466
|
-
params: { user_id_type: userIdType },
|
|
467
|
-
});
|
|
468
|
-
return data.data?.user ?? null;
|
|
469
|
-
} catch (error) {
|
|
470
|
-
this.#logger.error('Failed to get user info:', error);
|
|
471
|
-
return null;
|
|
472
|
-
}
|
|
22
|
+
#startFramework(signal: AbortSignal) { return this.connectTransport(signal); }
|
|
23
|
+
protected override receiveInboundCallback<T>(receive: () => T | Promise<T>): Promise<T> {
|
|
24
|
+
return this[endpointSpec].events.run(receive);
|
|
473
25
|
}
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
try {
|
|
477
|
-
const data = await this.#request(`/im/v1/chats/${chatId}`, { method: 'GET' });
|
|
478
|
-
return data.data ?? null;
|
|
479
|
-
} catch (error) {
|
|
480
|
-
this.#logger.error('Failed to get chat info:', error);
|
|
481
|
-
return null;
|
|
482
|
-
}
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
async uploadFile(
|
|
486
|
-
filePath: string,
|
|
487
|
-
fileType: 'image' | 'file' | 'video' | 'audio' = 'file',
|
|
488
|
-
): Promise<string | null> {
|
|
489
|
-
try {
|
|
490
|
-
await this.#ensureAccessToken();
|
|
491
|
-
const buf = await readFile(filePath);
|
|
492
|
-
const form = new FormData();
|
|
493
|
-
form.append('file', new Blob([buf]), basename(filePath));
|
|
494
|
-
form.append('file_type', fileType);
|
|
495
|
-
const url = `${this.#options.config.apiBaseUrl}/im/v1/files`;
|
|
496
|
-
const response = await this.#fetch(url, {
|
|
497
|
-
method: 'POST',
|
|
498
|
-
headers: { Authorization: `Bearer ${this.#accessToken.token}` },
|
|
499
|
-
body: form,
|
|
500
|
-
});
|
|
501
|
-
const data = await response.json() as LarkApiResponse;
|
|
502
|
-
if (data.code === 0) {
|
|
503
|
-
return (data.data?.file_key as string) || null;
|
|
504
|
-
}
|
|
505
|
-
throw new Error(`Upload failed: ${data.msg}`);
|
|
506
|
-
} catch (error) {
|
|
507
|
-
this.#logger.error('Failed to upload file:', error);
|
|
508
|
-
return null;
|
|
509
|
-
}
|
|
26
|
+
protected override publishEvent(name: string, payload: unknown): Promise<unknown> {
|
|
27
|
+
return this[endpointSpec].events.emit(name, payload);
|
|
510
28
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
name: string,
|
|
514
|
-
userIds: string[],
|
|
515
|
-
ownerId?: string,
|
|
516
|
-
): Promise<string | null> {
|
|
517
|
-
try {
|
|
518
|
-
const data = await this.#request('/im/v1/chats', {
|
|
519
|
-
method: 'POST',
|
|
520
|
-
body: {
|
|
521
|
-
name,
|
|
522
|
-
user_id_list: userIds,
|
|
523
|
-
...(ownerId ? { owner_id: ownerId } : {}),
|
|
524
|
-
},
|
|
525
|
-
});
|
|
526
|
-
if (data.code === 0) return (data.data?.chat_id as string) || null;
|
|
527
|
-
throw new Error(`Failed to create chat: ${data.msg}`);
|
|
528
|
-
} catch (error) {
|
|
529
|
-
this.#logger.error('Failed to create chat:', error);
|
|
530
|
-
return null;
|
|
531
|
-
}
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
async updateChatInfo(
|
|
535
|
-
chatId: string,
|
|
536
|
-
options: { name?: string; description?: string },
|
|
537
|
-
): Promise<boolean> {
|
|
538
|
-
try {
|
|
539
|
-
const data = await this.#request(`/im/v1/chats/${chatId}`, {
|
|
540
|
-
method: 'PUT',
|
|
541
|
-
body: options,
|
|
542
|
-
});
|
|
543
|
-
if (data.code === 0) return true;
|
|
544
|
-
throw new Error(`Failed to update chat: ${data.msg}`);
|
|
545
|
-
} catch (error) {
|
|
546
|
-
this.#logger.error('Failed to update chat:', error);
|
|
547
|
-
return false;
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
async addChatMembers(chatId: string, userIds: string[]): Promise<boolean> {
|
|
552
|
-
try {
|
|
553
|
-
const data = await this.#request(`/im/v1/chats/${chatId}/members`, {
|
|
554
|
-
method: 'POST',
|
|
555
|
-
body: { id_list: userIds },
|
|
556
|
-
});
|
|
557
|
-
if (data.code === 0) return true;
|
|
558
|
-
throw new Error(`Failed to add members: ${data.msg}`);
|
|
559
|
-
} catch (error) {
|
|
560
|
-
this.#logger.error('Failed to add chat members:', error);
|
|
561
|
-
return false;
|
|
562
|
-
}
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
async removeChatMembers(chatId: string, userIds: string[]): Promise<boolean> {
|
|
566
|
-
try {
|
|
567
|
-
const data = await this.#request(`/im/v1/chats/${chatId}/members`, {
|
|
568
|
-
method: 'DELETE',
|
|
569
|
-
body: { id_list: userIds },
|
|
570
|
-
});
|
|
571
|
-
if (data.code === 0) return true;
|
|
572
|
-
throw new Error(`Failed to remove members: ${data.msg}`);
|
|
573
|
-
} catch (error) {
|
|
574
|
-
this.#logger.error('Failed to remove chat members:', error);
|
|
575
|
-
return false;
|
|
576
|
-
}
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
async getChatMembers(chatId: string): Promise<unknown[]> {
|
|
580
|
-
try {
|
|
581
|
-
const data = await this.#request(`/im/v1/chats/${chatId}/members`, { method: 'GET' });
|
|
582
|
-
if (data.code === 0) return (data.data?.items as unknown[]) || [];
|
|
583
|
-
throw new Error(`Failed to get members: ${data.msg}`);
|
|
584
|
-
} catch (error) {
|
|
585
|
-
this.#logger.error('Failed to get chat members:', error);
|
|
586
|
-
return [];
|
|
587
|
-
}
|
|
29
|
+
protected override publishAccepted(name: string, payload: unknown): Promise<void> {
|
|
30
|
+
return this[endpointSpec].events.emitAccepted(name, payload).then(() => undefined);
|
|
588
31
|
}
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
const data = await this.#request(`/im/v1/chats/${chatId}`, { method: 'DELETE' });
|
|
593
|
-
if (data.code === 0) return true;
|
|
594
|
-
throw new Error(`Failed to dissolve chat: ${data.msg}`);
|
|
595
|
-
} catch (error) {
|
|
596
|
-
this.#logger.error('Failed to dissolve chat:', error);
|
|
597
|
-
return false;
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
|
|
601
|
-
async setChatManagers(chatId: string, userIds: string[]): Promise<boolean> {
|
|
602
|
-
try {
|
|
603
|
-
const data = await this.#request(`/im/v1/chats/${chatId}/managers/add_managers`, {
|
|
604
|
-
method: 'POST',
|
|
605
|
-
body: { manager_ids: userIds },
|
|
606
|
-
});
|
|
607
|
-
if (data.code === 0) return true;
|
|
608
|
-
throw new Error(`Failed to set managers: ${data.msg}`);
|
|
609
|
-
} catch (error) {
|
|
610
|
-
this.#logger.error('Failed to set chat managers:', error);
|
|
611
|
-
return false;
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
async removeChatManagers(chatId: string, userIds: string[]): Promise<boolean> {
|
|
616
|
-
try {
|
|
617
|
-
const data = await this.#request(`/im/v1/chats/${chatId}/managers/delete_managers`, {
|
|
618
|
-
method: 'POST',
|
|
619
|
-
body: { manager_ids: userIds },
|
|
620
|
-
});
|
|
621
|
-
if (data.code === 0) return true;
|
|
622
|
-
throw new Error(`Failed to remove managers: ${data.msg}`);
|
|
623
|
-
} catch (error) {
|
|
624
|
-
this.#logger.error('Failed to remove chat managers:', error);
|
|
625
|
-
return false;
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
|
|
629
|
-
/** 拉取分页列表(items + has_more/page_token);code!==0 时抛错交由 RPC 层透出。 */
|
|
630
|
-
async #listAllPages(
|
|
631
|
-
path: string,
|
|
632
|
-
params: Record<string, string | number> = {},
|
|
633
|
-
): Promise<Record<string, unknown>[]> {
|
|
634
|
-
const items: Record<string, unknown>[] = [];
|
|
635
|
-
let pageToken = '';
|
|
636
|
-
do {
|
|
637
|
-
const data = await this.#request(path, {
|
|
638
|
-
method: 'GET',
|
|
639
|
-
params: {
|
|
640
|
-
...params,
|
|
641
|
-
page_size: 100,
|
|
642
|
-
...(pageToken ? { page_token: pageToken } : {}),
|
|
643
|
-
},
|
|
644
|
-
});
|
|
645
|
-
if (data.code !== 0) {
|
|
646
|
-
throw new Error(`Lark API ${path} failed: ${data.msg ?? 'unknown'} (${data.code})`);
|
|
647
|
-
}
|
|
648
|
-
const page = data.data ?? {};
|
|
649
|
-
if (Array.isArray(page.items)) {
|
|
650
|
-
for (const item of page.items) {
|
|
651
|
-
if (item !== null && typeof item === 'object') {
|
|
652
|
-
items.push(item as Record<string, unknown>);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
}
|
|
656
|
-
pageToken = page.has_more === true && typeof page.page_token === 'string'
|
|
657
|
-
? page.page_token
|
|
658
|
-
: '';
|
|
659
|
-
} while (pageToken);
|
|
660
|
-
return items;
|
|
661
|
-
}
|
|
662
|
-
|
|
663
|
-
async #request(
|
|
664
|
-
path: string,
|
|
665
|
-
options: {
|
|
666
|
-
method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
|
667
|
-
params?: Record<string, string | number>;
|
|
668
|
-
body?: Record<string, unknown>;
|
|
669
|
-
} = {},
|
|
670
|
-
): Promise<LarkApiResponse> {
|
|
671
|
-
await this.#ensureAccessToken();
|
|
672
|
-
const { method = 'GET', params = {}, body } = options;
|
|
673
|
-
const urlParams = new URLSearchParams(
|
|
674
|
-
Object.fromEntries(
|
|
675
|
-
Object.entries(params).map(([key, value]) => [key, String(value)]),
|
|
676
|
-
),
|
|
677
|
-
);
|
|
678
|
-
const query = urlParams.toString();
|
|
679
|
-
const url = `${this.#options.config.apiBaseUrl}${path}${query ? `?${query}` : ''}`;
|
|
680
|
-
const response = await this.#fetch(url, {
|
|
681
|
-
method,
|
|
682
|
-
headers: {
|
|
683
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
684
|
-
Authorization: `Bearer ${this.#accessToken.token}`,
|
|
685
|
-
},
|
|
686
|
-
body: body && method !== 'GET' ? JSON.stringify(body) : undefined,
|
|
687
|
-
signal: AbortSignal.timeout(OUTBOUND_TIMEOUT_MS),
|
|
688
|
-
});
|
|
689
|
-
const data = await response.json().catch(() => undefined) as LarkApiResponse | undefined;
|
|
690
|
-
const platformCode = typeof data?.code === 'number' && Number.isFinite(data.code) ? data.code : undefined;
|
|
691
|
-
if (!response.ok) {
|
|
692
|
-
const rejected = response.status >= 400 && response.status < 500 && response.status !== 408;
|
|
693
|
-
throw new LarkRequestError(response.status, platformCode, rejected);
|
|
694
|
-
}
|
|
695
|
-
if (platformCode === undefined) throw new LarkRequestError(response.status, undefined, false);
|
|
696
|
-
if (platformCode !== 0) throw new LarkRequestError(response.status, platformCode, true);
|
|
697
|
-
return data!;
|
|
698
|
-
}
|
|
699
|
-
|
|
700
|
-
async #ensureAccessToken(): Promise<void> {
|
|
701
|
-
const now = Date.now();
|
|
702
|
-
if (
|
|
703
|
-
this.#accessToken.token
|
|
704
|
-
&& now < this.#accessToken.timestamp + (this.#accessToken.expires_in - 300) * 1000
|
|
705
|
-
) {
|
|
706
|
-
return;
|
|
707
|
-
}
|
|
708
|
-
if (this.#refreshPromise) {
|
|
709
|
-
await this.#refreshPromise;
|
|
710
|
-
return;
|
|
711
|
-
}
|
|
712
|
-
this.#refreshPromise = this.#refreshAccessToken()
|
|
713
|
-
.then(() => this.#accessToken.token)
|
|
714
|
-
.finally(() => { this.#refreshPromise = null; });
|
|
715
|
-
await this.#refreshPromise;
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
async #refreshAccessToken(): Promise<void> {
|
|
719
|
-
const { appId, appSecret, apiBaseUrl } = this.#options.config;
|
|
720
|
-
const url = `${apiBaseUrl}/auth/v3/tenant_access_token/internal`;
|
|
721
|
-
const response = await this.#fetch(url, {
|
|
722
|
-
method: 'POST',
|
|
723
|
-
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
724
|
-
body: JSON.stringify({ app_id: appId, app_secret: appSecret }),
|
|
725
|
-
});
|
|
726
|
-
const data = await response.json() as LarkApiResponse;
|
|
727
|
-
if (data.code === 0 && data.tenant_access_token) {
|
|
728
|
-
this.#accessToken = {
|
|
729
|
-
token: data.tenant_access_token,
|
|
730
|
-
expires_in: data.expire ?? 7200,
|
|
731
|
-
timestamp: Date.now(),
|
|
732
|
-
};
|
|
733
|
-
this.#logger.debug('Access token refreshed successfully');
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
throw new LarkRequestError(response.status, typeof data.code === 'number' ? data.code : undefined, response.ok && typeof data.code === 'number' && data.code !== 0 || response.status >= 400 && response.status < 500 && response.status !== 408);
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
|
|
740
|
-
function requireMessageId(value: unknown): string {
|
|
741
|
-
if (typeof value === 'string' && value.trim().length > 0) return value;
|
|
742
|
-
throw new EndpointDeliveryError('delivery_unconfirmed', 'Platform did not return a real message ID', 'unknown');
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
/** Carries only safe numerical diagnostics, never response bodies or credentials. */
|
|
746
|
-
class LarkRequestError extends EndpointDeliveryError {
|
|
747
|
-
readonly status: number;
|
|
748
|
-
readonly platformCode: number | undefined;
|
|
749
|
-
constructor(status: number, platformCode: number | undefined, rejected: boolean) {
|
|
750
|
-
super(rejected ? 'platform_rejected' : 'delivery_unconfirmed',
|
|
751
|
-
`Lark request failed (status=${status}${platformCode === undefined ? '' : `, code=${platformCode}`})`,
|
|
752
|
-
rejected ? 'rejected' : 'unknown');
|
|
753
|
-
this.status = status;
|
|
754
|
-
this.platformCode = platformCode;
|
|
32
|
+
protected override async publishPlatform(name: string, event: unknown): Promise<unknown> {
|
|
33
|
+
await super.publishPlatform(name, event);
|
|
34
|
+
return this[endpointSpec].events.platform(name, event);
|
|
755
35
|
}
|
|
756
36
|
}
|