eve-lark 0.1.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/LICENSE +21 -0
- package/README.md +222 -0
- package/README.zh-CN.md +222 -0
- package/dist/index.d.ts +258 -0
- package/dist/index.js +1075 -0
- package/dist/index.js.map +1 -0
- package/package.json +69 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1075 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
3
|
+
|
|
4
|
+
// src/channel.ts
|
|
5
|
+
import {
|
|
6
|
+
defineChannel,
|
|
7
|
+
POST
|
|
8
|
+
} from "eve/channels";
|
|
9
|
+
|
|
10
|
+
// src/errors.ts
|
|
11
|
+
var LarkChannelError = class extends Error {
|
|
12
|
+
static {
|
|
13
|
+
__name(this, "LarkChannelError");
|
|
14
|
+
}
|
|
15
|
+
constructor(message, options) {
|
|
16
|
+
super(message, options);
|
|
17
|
+
this.name = new.target.name;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
var LarkConfigError = class extends LarkChannelError {
|
|
21
|
+
static {
|
|
22
|
+
__name(this, "LarkConfigError");
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
var LarkSignatureError = class extends LarkChannelError {
|
|
26
|
+
static {
|
|
27
|
+
__name(this, "LarkSignatureError");
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
var LarkDecryptError = class extends LarkChannelError {
|
|
31
|
+
static {
|
|
32
|
+
__name(this, "LarkDecryptError");
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
var LarkApiError = class extends LarkChannelError {
|
|
36
|
+
static {
|
|
37
|
+
__name(this, "LarkApiError");
|
|
38
|
+
}
|
|
39
|
+
code;
|
|
40
|
+
body;
|
|
41
|
+
status;
|
|
42
|
+
constructor(message, opts) {
|
|
43
|
+
super(message, { cause: opts?.cause });
|
|
44
|
+
this.code = opts?.code;
|
|
45
|
+
this.body = opts?.body;
|
|
46
|
+
this.status = opts?.status;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/lark-client.ts
|
|
51
|
+
var TOKEN_INVALID_CODES = /* @__PURE__ */ new Set([99991663, 99991664, 99991661]);
|
|
52
|
+
var LarkClient = class {
|
|
53
|
+
static {
|
|
54
|
+
__name(this, "LarkClient");
|
|
55
|
+
}
|
|
56
|
+
options;
|
|
57
|
+
token = null;
|
|
58
|
+
refreshPromise = null;
|
|
59
|
+
constructor(options) {
|
|
60
|
+
this.options = options;
|
|
61
|
+
}
|
|
62
|
+
async getTenantAccessToken() {
|
|
63
|
+
if (this.token && Date.now() + this.options.tokenRefreshBufferMs < this.token.expiresAt) {
|
|
64
|
+
return this.token.value;
|
|
65
|
+
}
|
|
66
|
+
if (this.refreshPromise) return this.refreshPromise;
|
|
67
|
+
this.refreshPromise = this.#refresh();
|
|
68
|
+
try {
|
|
69
|
+
return await this.refreshPromise;
|
|
70
|
+
} finally {
|
|
71
|
+
this.refreshPromise = null;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
async #refresh() {
|
|
75
|
+
const body = {
|
|
76
|
+
app_id: this.options.appId,
|
|
77
|
+
app_secret: this.options.appSecret
|
|
78
|
+
};
|
|
79
|
+
const res = await this.options.fetch(
|
|
80
|
+
`${this.options.baseUrl}/open-apis/auth/v3/tenant_access_token/internal`,
|
|
81
|
+
{
|
|
82
|
+
method: "POST",
|
|
83
|
+
headers: { "content-type": "application/json" },
|
|
84
|
+
body: JSON.stringify(body),
|
|
85
|
+
signal: AbortSignal.timeout(this.options.requestTimeoutMs)
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
if (!res.ok) {
|
|
89
|
+
throw new LarkApiError(
|
|
90
|
+
`eve-lark: token refresh failed (HTTP ${res.status})`,
|
|
91
|
+
{ status: res.status }
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
const json = await res.json();
|
|
95
|
+
if (json.code !== 0 || !json.tenant_access_token) {
|
|
96
|
+
throw new LarkApiError(
|
|
97
|
+
`eve-lark: token refresh returned code=${json.code ?? "?"} msg=${json.msg ?? "?"}`,
|
|
98
|
+
{ body: json, code: json.code }
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
const expireSec = typeof json.expire === "number" ? json.expire : 7200;
|
|
102
|
+
this.token = {
|
|
103
|
+
value: json.tenant_access_token,
|
|
104
|
+
expiresAt: Date.now() + expireSec * 1e3
|
|
105
|
+
};
|
|
106
|
+
return this.token.value;
|
|
107
|
+
}
|
|
108
|
+
async sendText(args) {
|
|
109
|
+
const content = JSON.stringify({ text: args.content });
|
|
110
|
+
return this.#sendMessage({
|
|
111
|
+
receive_id: args.chatId,
|
|
112
|
+
msg_type: "text",
|
|
113
|
+
content,
|
|
114
|
+
root_id: args.rootId,
|
|
115
|
+
parent_id: args.parentId
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
async sendCard(args) {
|
|
119
|
+
const content = JSON.stringify(args.card);
|
|
120
|
+
return this.#sendMessage({
|
|
121
|
+
receive_id: args.chatId,
|
|
122
|
+
msg_type: "interactive",
|
|
123
|
+
content,
|
|
124
|
+
root_id: args.rootId,
|
|
125
|
+
parent_id: args.parentId
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
async #sendMessage(body) {
|
|
129
|
+
const payload = Object.fromEntries(
|
|
130
|
+
Object.entries(body).filter(([, v]) => v !== void 0)
|
|
131
|
+
);
|
|
132
|
+
const json = await this.#request("POST", "/open-apis/im/v1/messages?receive_id_type=chat_id", payload);
|
|
133
|
+
const messageId = json.data?.message_id;
|
|
134
|
+
if (!messageId) {
|
|
135
|
+
throw new LarkApiError("eve-lark: send missing message_id in response", {
|
|
136
|
+
body: json
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
return { messageId };
|
|
140
|
+
}
|
|
141
|
+
async patchCard(args) {
|
|
142
|
+
await this.#request(
|
|
143
|
+
"PATCH",
|
|
144
|
+
`/open-apis/im/v1/messages/${encodeURIComponent(args.messageId)}`,
|
|
145
|
+
{ content: JSON.stringify(args.card) }
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
async downloadResource(args) {
|
|
149
|
+
const path = `/open-apis/im/v1/messages/${encodeURIComponent(args.messageId)}/resources/${encodeURIComponent(args.fileKey)}?type=${args.type}`;
|
|
150
|
+
const token = await this.getTenantAccessToken();
|
|
151
|
+
const res = await this.options.fetch(`${this.options.baseUrl}${path}`, {
|
|
152
|
+
method: "GET",
|
|
153
|
+
headers: { authorization: `Bearer ${token}` },
|
|
154
|
+
signal: AbortSignal.timeout(this.options.requestTimeoutMs)
|
|
155
|
+
});
|
|
156
|
+
if (!res.ok) {
|
|
157
|
+
throw new LarkApiError(
|
|
158
|
+
`eve-lark: downloadResource HTTP ${res.status}`,
|
|
159
|
+
{ status: res.status }
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
return Buffer.from(await res.arrayBuffer());
|
|
163
|
+
}
|
|
164
|
+
async addReaction(args) {
|
|
165
|
+
const path = `/open-apis/im/v1/messages/${encodeURIComponent(args.messageId)}/reactions`;
|
|
166
|
+
const json = await this.#request("POST", path, {
|
|
167
|
+
reaction_type: { emoji_type: args.emojiType }
|
|
168
|
+
});
|
|
169
|
+
const reactionId = json.data?.reaction_id;
|
|
170
|
+
if (!reactionId) {
|
|
171
|
+
throw new LarkApiError("eve-lark: addReaction missing reaction_id", {
|
|
172
|
+
body: json
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
return { reactionId };
|
|
176
|
+
}
|
|
177
|
+
async removeReaction(args) {
|
|
178
|
+
const path = `/open-apis/im/v1/messages/${encodeURIComponent(args.messageId)}/reactions/${encodeURIComponent(args.reactionId)}`;
|
|
179
|
+
await this.#request("DELETE", path, void 0);
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Central request wrapper with auth, retry, and Feishu error decoding.
|
|
183
|
+
*
|
|
184
|
+
* Retries on 429 (honoring Retry-After), 5xx (exponential backoff + jitter),
|
|
185
|
+
* and token-invalid responses (one refresh + one retry). Other 4xx throws
|
|
186
|
+
* LarkApiError with the Feishu code/msg from the body.
|
|
187
|
+
*/
|
|
188
|
+
async #request(method, path, body) {
|
|
189
|
+
const url = `${this.options.baseUrl}${path}`;
|
|
190
|
+
let token = await this.getTenantAccessToken();
|
|
191
|
+
let tokenRefreshed = false;
|
|
192
|
+
for (let attempt = 0; attempt <= this.options.maxRetries; attempt++) {
|
|
193
|
+
const res = await this.options.fetch(url, {
|
|
194
|
+
method,
|
|
195
|
+
headers: {
|
|
196
|
+
authorization: `Bearer ${token}`,
|
|
197
|
+
"content-type": "application/json"
|
|
198
|
+
},
|
|
199
|
+
body: body === void 0 ? void 0 : JSON.stringify(body),
|
|
200
|
+
signal: AbortSignal.timeout(this.options.requestTimeoutMs)
|
|
201
|
+
});
|
|
202
|
+
const result = await this.#consumeResponse(res);
|
|
203
|
+
const status = result.status;
|
|
204
|
+
if (status >= 200 && status < 300) {
|
|
205
|
+
const jsonBody = result.body;
|
|
206
|
+
if (jsonBody && typeof jsonBody.code === "number" && jsonBody.code !== 0) {
|
|
207
|
+
if (TOKEN_INVALID_CODES.has(jsonBody.code) && !tokenRefreshed) {
|
|
208
|
+
this.token = null;
|
|
209
|
+
token = await this.getTenantAccessToken();
|
|
210
|
+
tokenRefreshed = true;
|
|
211
|
+
attempt -= 1;
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
throw new LarkApiError(
|
|
215
|
+
`eve-lark: ${method} ${path} failed code=${jsonBody.code} msg=${jsonBody.msg ?? "?"}`,
|
|
216
|
+
{ code: jsonBody.code, body: jsonBody, status }
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
return result.body;
|
|
220
|
+
}
|
|
221
|
+
if (status === 401 && !tokenRefreshed) {
|
|
222
|
+
this.token = null;
|
|
223
|
+
token = await this.getTenantAccessToken();
|
|
224
|
+
tokenRefreshed = true;
|
|
225
|
+
attempt -= 1;
|
|
226
|
+
continue;
|
|
227
|
+
}
|
|
228
|
+
const retryable = status === 429 || status >= 500 && status < 600;
|
|
229
|
+
if (retryable && attempt < this.options.maxRetries) {
|
|
230
|
+
const delayMs = this.#computeBackoff(status, result.retryAfter, attempt);
|
|
231
|
+
await sleep(delayMs);
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
const bodyObj = result.body;
|
|
235
|
+
const code = bodyObj?.code;
|
|
236
|
+
const msg = bodyObj?.msg;
|
|
237
|
+
const detail = msg ? ` code=${code ?? "?"} msg=${msg}` : "";
|
|
238
|
+
throw new LarkApiError(
|
|
239
|
+
`eve-lark: ${method} ${path} failed HTTP ${status}${detail}`,
|
|
240
|
+
{ status, body: bodyObj, code }
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
throw new LarkApiError(`eve-lark: ${method} ${path} exhausted retries`);
|
|
244
|
+
}
|
|
245
|
+
async #consumeResponse(res) {
|
|
246
|
+
const retryAfterRaw = res.headers.get("retry-after");
|
|
247
|
+
const retryAfter = retryAfterRaw ? parseRetryAfter(retryAfterRaw) : null;
|
|
248
|
+
const text = await res.text();
|
|
249
|
+
if (!text) {
|
|
250
|
+
return { status: res.status, body: void 0, retryAfter };
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
return { status: res.status, body: JSON.parse(text), retryAfter };
|
|
254
|
+
} catch {
|
|
255
|
+
return { status: res.status, body: { raw: text }, retryAfter };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
#computeBackoff(status, retryAfter, attempt) {
|
|
259
|
+
if (status === 429 && retryAfter !== null) {
|
|
260
|
+
return Math.min(retryAfter * 1e3, 1e4);
|
|
261
|
+
}
|
|
262
|
+
const base = 300 * Math.pow(2, attempt);
|
|
263
|
+
const jitter = base * 0.2 * (Math.random() * 2 - 1);
|
|
264
|
+
return Math.max(0, Math.round(base + jitter));
|
|
265
|
+
}
|
|
266
|
+
};
|
|
267
|
+
function parseRetryAfter(raw) {
|
|
268
|
+
const sec = Number(raw);
|
|
269
|
+
if (Number.isFinite(sec) && sec >= 0) return sec;
|
|
270
|
+
const date = Date.parse(raw);
|
|
271
|
+
if (Number.isFinite(date)) return Math.max(0, (date - Date.now()) / 1e3);
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
__name(parseRetryAfter, "parseRetryAfter");
|
|
275
|
+
function sleep(ms) {
|
|
276
|
+
if (ms <= 0) return Promise.resolve();
|
|
277
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
278
|
+
}
|
|
279
|
+
__name(sleep, "sleep");
|
|
280
|
+
|
|
281
|
+
// src/dedup.ts
|
|
282
|
+
var DedupMap = class {
|
|
283
|
+
static {
|
|
284
|
+
__name(this, "DedupMap");
|
|
285
|
+
}
|
|
286
|
+
entries = /* @__PURE__ */ new Map();
|
|
287
|
+
ttlMs;
|
|
288
|
+
maxEntries;
|
|
289
|
+
insertsSinceSweep = 0;
|
|
290
|
+
constructor(ttlMs, maxEntries) {
|
|
291
|
+
this.ttlMs = ttlMs;
|
|
292
|
+
this.maxEntries = maxEntries;
|
|
293
|
+
}
|
|
294
|
+
has(key) {
|
|
295
|
+
const at = this.entries.get(key);
|
|
296
|
+
if (at === void 0) return false;
|
|
297
|
+
if (Date.now() - at > this.ttlMs) {
|
|
298
|
+
this.entries.delete(key);
|
|
299
|
+
return false;
|
|
300
|
+
}
|
|
301
|
+
return true;
|
|
302
|
+
}
|
|
303
|
+
set(key) {
|
|
304
|
+
this.maybeSweep();
|
|
305
|
+
this.entries.delete(key);
|
|
306
|
+
this.entries.set(key, Date.now());
|
|
307
|
+
while (this.entries.size > this.maxEntries) {
|
|
308
|
+
const oldestKey = this.entries.keys().next().value;
|
|
309
|
+
if (oldestKey === void 0) break;
|
|
310
|
+
this.entries.delete(oldestKey);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* Walk the insertion-ordered map from the front and drop expired entries.
|
|
315
|
+
* Stops at the first non-expired entry since events arrive roughly in time
|
|
316
|
+
* order. Called on every set, so cost is amortized.
|
|
317
|
+
*/
|
|
318
|
+
maybeSweep() {
|
|
319
|
+
this.insertsSinceSweep += 1;
|
|
320
|
+
if (this.insertsSinceSweep < 64 && this.entries.size < this.maxEntries) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
this.insertsSinceSweep = 0;
|
|
324
|
+
const now = Date.now();
|
|
325
|
+
for (const [key, at] of this.entries) {
|
|
326
|
+
if (now - at <= this.ttlMs) break;
|
|
327
|
+
this.entries.delete(key);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
};
|
|
331
|
+
|
|
332
|
+
// src/crypto.ts
|
|
333
|
+
import { createDecipheriv, createHash, timingSafeEqual } from "crypto";
|
|
334
|
+
function verifySignature(opts) {
|
|
335
|
+
const expected = opts.signatureHeader.replace(/^sha256=/, "");
|
|
336
|
+
const computed = createHash("sha256").update(opts.timestamp + opts.nonce + opts.encryptKey).update(opts.rawBody).digest("hex");
|
|
337
|
+
const a = Buffer.from(computed, "hex");
|
|
338
|
+
const b = Buffer.from(expected, "hex");
|
|
339
|
+
return a.length === b.length && timingSafeEqual(a, b);
|
|
340
|
+
}
|
|
341
|
+
__name(verifySignature, "verifySignature");
|
|
342
|
+
function decryptPayload(encryptB64, encryptKey) {
|
|
343
|
+
const key = createHash("sha256").update(encryptKey).digest();
|
|
344
|
+
const buf = Buffer.from(encryptB64, "base64");
|
|
345
|
+
if (buf.length < 32) {
|
|
346
|
+
throw new LarkDecryptError(
|
|
347
|
+
`eve-lark: ciphertext too short (${buf.length} bytes; need >= 32 for IV + one block)`
|
|
348
|
+
);
|
|
349
|
+
}
|
|
350
|
+
if ((buf.length - 16) % 16 !== 0) {
|
|
351
|
+
throw new LarkDecryptError(
|
|
352
|
+
`eve-lark: ciphertext length ${buf.length} is not 16 + N*16`
|
|
353
|
+
);
|
|
354
|
+
}
|
|
355
|
+
const iv = buf.subarray(0, 16);
|
|
356
|
+
const ct = buf.subarray(16);
|
|
357
|
+
const dec = createDecipheriv("aes-256-cbc", key, iv);
|
|
358
|
+
try {
|
|
359
|
+
return Buffer.concat([dec.update(ct), dec.final()]);
|
|
360
|
+
} catch (e) {
|
|
361
|
+
throw new LarkDecryptError("eve-lark: AES decrypt failed (bad padding or wrong key)", {
|
|
362
|
+
cause: e
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
__name(decryptPayload, "decryptPayload");
|
|
367
|
+
|
|
368
|
+
// src/parse.ts
|
|
369
|
+
var MIME_BY_EXT = {
|
|
370
|
+
pdf: "application/pdf",
|
|
371
|
+
zip: "application/zip",
|
|
372
|
+
gz: "application/gzip",
|
|
373
|
+
tar: "application/x-tar",
|
|
374
|
+
doc: "application/msword",
|
|
375
|
+
docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
376
|
+
xls: "application/vnd.ms-excel",
|
|
377
|
+
xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
378
|
+
ppt: "application/vnd.ms-powerpoint",
|
|
379
|
+
pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
380
|
+
csv: "text/csv",
|
|
381
|
+
txt: "text/plain",
|
|
382
|
+
md: "text/markdown",
|
|
383
|
+
json: "application/json",
|
|
384
|
+
xml: "application/xml",
|
|
385
|
+
html: "text/html",
|
|
386
|
+
htm: "text/html",
|
|
387
|
+
png: "image/png",
|
|
388
|
+
jpg: "image/jpeg",
|
|
389
|
+
jpeg: "image/jpeg",
|
|
390
|
+
gif: "image/gif",
|
|
391
|
+
webp: "image/webp",
|
|
392
|
+
mp3: "audio/mpeg",
|
|
393
|
+
wav: "audio/wav",
|
|
394
|
+
mp4: "video/mp4",
|
|
395
|
+
mov: "video/quicktime"
|
|
396
|
+
};
|
|
397
|
+
function mimeFromExt(filename) {
|
|
398
|
+
if (!filename) return "application/octet-stream";
|
|
399
|
+
const dot = filename.lastIndexOf(".");
|
|
400
|
+
if (dot < 0) return "application/octet-stream";
|
|
401
|
+
return MIME_BY_EXT[filename.slice(dot + 1).toLowerCase()] ?? "application/octet-stream";
|
|
402
|
+
}
|
|
403
|
+
__name(mimeFromExt, "mimeFromExt");
|
|
404
|
+
function mentionFromRaw(m, botOpenId) {
|
|
405
|
+
const isOpenIdOfBot = !!botOpenId && !!m.id.open_id && m.id.open_id === botOpenId;
|
|
406
|
+
const isAll = !!m.id.open_id && m.id.open_id === "all";
|
|
407
|
+
return {
|
|
408
|
+
key: m.key,
|
|
409
|
+
id: {
|
|
410
|
+
openId: m.id.open_id,
|
|
411
|
+
userId: m.id.user_id,
|
|
412
|
+
unionId: m.id.union_id
|
|
413
|
+
},
|
|
414
|
+
name: m.name,
|
|
415
|
+
idType: m.id_type ?? "open_id",
|
|
416
|
+
isOpenIdOfBot,
|
|
417
|
+
isAll
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
__name(mentionFromRaw, "mentionFromRaw");
|
|
421
|
+
function stripBotMentions(text, mentions) {
|
|
422
|
+
let out = text;
|
|
423
|
+
for (const m of mentions) {
|
|
424
|
+
if (!m.key) continue;
|
|
425
|
+
if (m.isOpenIdOfBot) {
|
|
426
|
+
out = out.split(m.key).join("");
|
|
427
|
+
} else if (m.isAll) {
|
|
428
|
+
out = out.split(m.key).join("@all");
|
|
429
|
+
} else {
|
|
430
|
+
out = out.split(m.key).join(`@${m.name}`);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
return out.replace(/\s+/g, " ").trim();
|
|
434
|
+
}
|
|
435
|
+
__name(stripBotMentions, "stripBotMentions");
|
|
436
|
+
function parseContent(messageType, rawContent) {
|
|
437
|
+
if (!rawContent) return { text: "", files: [] };
|
|
438
|
+
let content;
|
|
439
|
+
try {
|
|
440
|
+
content = JSON.parse(rawContent);
|
|
441
|
+
} catch {
|
|
442
|
+
return { text: "", files: [] };
|
|
443
|
+
}
|
|
444
|
+
switch (messageType) {
|
|
445
|
+
case "text": {
|
|
446
|
+
const text = typeof content.text === "string" ? content.text : "";
|
|
447
|
+
return { text, files: [] };
|
|
448
|
+
}
|
|
449
|
+
case "image": {
|
|
450
|
+
const imageKey = typeof content.image_key === "string" ? content.image_key : "";
|
|
451
|
+
if (!imageKey) return { text: "", files: [] };
|
|
452
|
+
return {
|
|
453
|
+
text: "",
|
|
454
|
+
files: [{ fileKey: imageKey, mediaType: "image/png", kind: "image" }]
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
case "file": {
|
|
458
|
+
const fileKey = typeof content.file_key === "string" ? content.file_key : "";
|
|
459
|
+
if (!fileKey) return { text: "", files: [] };
|
|
460
|
+
const fileName = typeof content.file_name === "string" ? content.file_name : void 0;
|
|
461
|
+
return {
|
|
462
|
+
text: "",
|
|
463
|
+
files: [{ fileKey, mediaType: mimeFromExt(fileName), kind: "file" }]
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
case "post": {
|
|
467
|
+
const locale = content.zh_cn ?? content.en_us ?? content.ja_jp ?? null;
|
|
468
|
+
if (!locale?.content) return { text: "", files: [] };
|
|
469
|
+
const text = locale.content.flatMap(
|
|
470
|
+
(line) => (line ?? []).filter((node) => {
|
|
471
|
+
if (typeof node !== "object" || node === null) return false;
|
|
472
|
+
const tag = node.tag;
|
|
473
|
+
const text2 = node.text;
|
|
474
|
+
return tag === "text" && typeof text2 === "string";
|
|
475
|
+
}).map((node) => node.text)
|
|
476
|
+
).join(" ");
|
|
477
|
+
return { text, files: [] };
|
|
478
|
+
}
|
|
479
|
+
default:
|
|
480
|
+
return { text: "", files: [] };
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
__name(parseContent, "parseContent");
|
|
484
|
+
function parseInbound(event, botOpenId) {
|
|
485
|
+
const messageType = event.message.message_type;
|
|
486
|
+
const parsed = parseContent(messageType, event.message.content);
|
|
487
|
+
const rawMentions = event.message.mentions ?? [];
|
|
488
|
+
const mentions = rawMentions.map((m) => mentionFromRaw(m, botOpenId));
|
|
489
|
+
const senderOpenId = event.sender.sender_id.open_id ?? event.sender.sender_id.user_id ?? event.sender.sender_id.union_id ?? "";
|
|
490
|
+
const text = messageType === "text" ? stripBotMentions(parsed.text, mentions) : parsed.text;
|
|
491
|
+
const chatType = event.chat_type === "group" ? "group" : "p2p";
|
|
492
|
+
const senderType = event.sender.sender_type === "app" ? "app" : "user";
|
|
493
|
+
return {
|
|
494
|
+
text,
|
|
495
|
+
files: parsed.files,
|
|
496
|
+
chatId: event.message.chat_id,
|
|
497
|
+
rootId: event.message.root_id ?? null,
|
|
498
|
+
parentId: event.message.parent_id ?? null,
|
|
499
|
+
messageId: event.message.message_id,
|
|
500
|
+
senderOpenId,
|
|
501
|
+
senderType,
|
|
502
|
+
chatType,
|
|
503
|
+
mentions
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
__name(parseInbound, "parseInbound");
|
|
507
|
+
|
|
508
|
+
// src/card.ts
|
|
509
|
+
var BASE_CONFIG = {
|
|
510
|
+
wide_screen_mode: true,
|
|
511
|
+
update_multi: true
|
|
512
|
+
};
|
|
513
|
+
function buildTextCard(text) {
|
|
514
|
+
return {
|
|
515
|
+
config: { ...BASE_CONFIG },
|
|
516
|
+
elements: [{ tag: "markdown", content: text }]
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
__name(buildTextCard, "buildTextCard");
|
|
520
|
+
function buildStreamingCard(opts) {
|
|
521
|
+
const lines = [];
|
|
522
|
+
if (opts.status) {
|
|
523
|
+
lines.push(`<font color='grey'>${opts.status}</font>`);
|
|
524
|
+
}
|
|
525
|
+
lines.push(opts.buffer.length > 0 ? opts.buffer : "_\u2026_");
|
|
526
|
+
return {
|
|
527
|
+
config: { ...BASE_CONFIG },
|
|
528
|
+
elements: [{ tag: "markdown", content: lines.join("\n\n") }]
|
|
529
|
+
};
|
|
530
|
+
}
|
|
531
|
+
__name(buildStreamingCard, "buildStreamingCard");
|
|
532
|
+
function buildErrorCard(message) {
|
|
533
|
+
return {
|
|
534
|
+
config: { ...BASE_CONFIG },
|
|
535
|
+
elements: [
|
|
536
|
+
{ tag: "markdown", content: `<font color='red'>\u26A0 ${message}</font>` }
|
|
537
|
+
]
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
__name(buildErrorCard, "buildErrorCard");
|
|
541
|
+
|
|
542
|
+
// src/streaming-controller.ts
|
|
543
|
+
var StreamingCardController = class {
|
|
544
|
+
static {
|
|
545
|
+
__name(this, "StreamingCardController");
|
|
546
|
+
}
|
|
547
|
+
deps;
|
|
548
|
+
client;
|
|
549
|
+
state = "idle";
|
|
550
|
+
buffer = "";
|
|
551
|
+
status;
|
|
552
|
+
messageId;
|
|
553
|
+
fallbackToText = false;
|
|
554
|
+
createTimer = null;
|
|
555
|
+
patchInFlight = null;
|
|
556
|
+
patchScheduled = false;
|
|
557
|
+
lastPatchAt = 0;
|
|
558
|
+
constructor(client, deps) {
|
|
559
|
+
this.client = client;
|
|
560
|
+
this.deps = deps;
|
|
561
|
+
}
|
|
562
|
+
appendDelta(text) {
|
|
563
|
+
if (this.state === "completed" || this.state === "aborted") return;
|
|
564
|
+
this.buffer += text;
|
|
565
|
+
if (this.state === "idle") {
|
|
566
|
+
this.scheduleCreate();
|
|
567
|
+
} else if (this.state === "streaming") {
|
|
568
|
+
this.schedulePatch();
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
setStatus(status) {
|
|
572
|
+
if (this.state === "completed" || this.state === "aborted") return;
|
|
573
|
+
this.status = status;
|
|
574
|
+
if (this.state === "streaming") {
|
|
575
|
+
this.schedulePatch();
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
async finalize(fullText) {
|
|
579
|
+
if (this.state === "completed" || this.state === "aborted") return;
|
|
580
|
+
this.cancelCreateTimer();
|
|
581
|
+
this.buffer = fullText;
|
|
582
|
+
if (this.fallbackToText) {
|
|
583
|
+
await this.client.sendText({
|
|
584
|
+
chatId: this.deps.chatId,
|
|
585
|
+
content: fullText,
|
|
586
|
+
rootId: this.deps.rootId,
|
|
587
|
+
parentId: this.deps.parentId
|
|
588
|
+
});
|
|
589
|
+
this.state = "completed";
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
if (this.messageId === void 0) {
|
|
593
|
+
try {
|
|
594
|
+
const res = await this.client.sendCard({
|
|
595
|
+
chatId: this.deps.chatId,
|
|
596
|
+
card: buildTextCard(fullText),
|
|
597
|
+
rootId: this.deps.rootId,
|
|
598
|
+
parentId: this.deps.parentId
|
|
599
|
+
});
|
|
600
|
+
this.messageId = res.messageId;
|
|
601
|
+
this.state = "completed";
|
|
602
|
+
} catch {
|
|
603
|
+
this.fallbackToText = true;
|
|
604
|
+
await this.client.sendText({
|
|
605
|
+
chatId: this.deps.chatId,
|
|
606
|
+
content: fullText,
|
|
607
|
+
rootId: this.deps.rootId,
|
|
608
|
+
parentId: this.deps.parentId
|
|
609
|
+
});
|
|
610
|
+
this.state = "completed";
|
|
611
|
+
}
|
|
612
|
+
return;
|
|
613
|
+
}
|
|
614
|
+
if (this.patchInFlight) {
|
|
615
|
+
try {
|
|
616
|
+
await this.patchInFlight;
|
|
617
|
+
} catch {
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
await this.client.patchCard({
|
|
621
|
+
messageId: this.messageId,
|
|
622
|
+
card: buildStreamingCard({ buffer: fullText, status: void 0 })
|
|
623
|
+
});
|
|
624
|
+
this.state = "completed";
|
|
625
|
+
}
|
|
626
|
+
async abort(error) {
|
|
627
|
+
if (this.state === "completed" || this.state === "aborted") return;
|
|
628
|
+
this.cancelCreateTimer();
|
|
629
|
+
if (this.messageId === void 0) {
|
|
630
|
+
this.fallbackToText = true;
|
|
631
|
+
this.state = "aborted";
|
|
632
|
+
return;
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
await this.client.patchCard({
|
|
636
|
+
messageId: this.messageId,
|
|
637
|
+
card: buildErrorCard(error)
|
|
638
|
+
});
|
|
639
|
+
} finally {
|
|
640
|
+
this.state = "aborted";
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
async ensureFinalized() {
|
|
644
|
+
if (this.state !== "completed" && this.state !== "aborted") {
|
|
645
|
+
await this.finalize(this.buffer);
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
isStreaming() {
|
|
649
|
+
return this.state === "streaming" || this.state === "creating";
|
|
650
|
+
}
|
|
651
|
+
isCompleted() {
|
|
652
|
+
return this.state === "completed" || this.state === "aborted";
|
|
653
|
+
}
|
|
654
|
+
scheduleCreate() {
|
|
655
|
+
if (this.createTimer) return;
|
|
656
|
+
this.state = "creating";
|
|
657
|
+
this.createTimer = setTimeout(() => {
|
|
658
|
+
this.createTimer = null;
|
|
659
|
+
void this.doCreate();
|
|
660
|
+
}, this.deps.createThresholdMs);
|
|
661
|
+
}
|
|
662
|
+
cancelCreateTimer() {
|
|
663
|
+
if (this.createTimer) {
|
|
664
|
+
clearTimeout(this.createTimer);
|
|
665
|
+
this.createTimer = null;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
async doCreate() {
|
|
669
|
+
if (this.state !== "creating") return;
|
|
670
|
+
try {
|
|
671
|
+
const res = await this.client.sendCard({
|
|
672
|
+
chatId: this.deps.chatId,
|
|
673
|
+
card: buildStreamingCard({ buffer: this.buffer, status: this.status }),
|
|
674
|
+
rootId: this.deps.rootId,
|
|
675
|
+
parentId: this.deps.parentId
|
|
676
|
+
});
|
|
677
|
+
this.messageId = res.messageId;
|
|
678
|
+
this.state = "streaming";
|
|
679
|
+
this.lastPatchAt = Date.now();
|
|
680
|
+
} catch {
|
|
681
|
+
this.fallbackToText = true;
|
|
682
|
+
this.state = "streaming";
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
schedulePatch() {
|
|
686
|
+
if (this.patchScheduled) return;
|
|
687
|
+
this.patchScheduled = true;
|
|
688
|
+
const elapsed = Date.now() - this.lastPatchAt;
|
|
689
|
+
const wait = Math.max(0, this.deps.patchIntervalMs - elapsed);
|
|
690
|
+
setTimeout(() => {
|
|
691
|
+
this.patchScheduled = false;
|
|
692
|
+
void this.maybeFlushPatch();
|
|
693
|
+
}, wait);
|
|
694
|
+
}
|
|
695
|
+
async maybeFlushPatch() {
|
|
696
|
+
if (this.state !== "streaming") return;
|
|
697
|
+
if (this.patchInFlight) return;
|
|
698
|
+
if (this.messageId === void 0) return;
|
|
699
|
+
const card = buildStreamingCard({ buffer: this.buffer, status: this.status });
|
|
700
|
+
this.patchInFlight = this.client.patchCard({ messageId: this.messageId, card }).catch(() => {
|
|
701
|
+
}).finally(() => {
|
|
702
|
+
this.patchInFlight = null;
|
|
703
|
+
this.lastPatchAt = Date.now();
|
|
704
|
+
});
|
|
705
|
+
await this.patchInFlight;
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
// src/options.ts
|
|
710
|
+
var DEFAULTS = {
|
|
711
|
+
baseUrl: "https://open.feishu.cn",
|
|
712
|
+
webhookPath: "/lark/webhook",
|
|
713
|
+
replyMode: "streaming",
|
|
714
|
+
streamPatchIntervalMs: 1e3,
|
|
715
|
+
streamCreateThresholdMs: 400,
|
|
716
|
+
dedupTtlMs: 30 * 60 * 1e3,
|
|
717
|
+
dedupMaxEntries: 5e3,
|
|
718
|
+
requestTimeoutMs: 15e3,
|
|
719
|
+
maxRetries: 2,
|
|
720
|
+
tokenRefreshBufferMs: 5 * 60 * 1e3,
|
|
721
|
+
signatureSkewMs: 5 * 60 * 1e3,
|
|
722
|
+
ackReaction: "TYPING"
|
|
723
|
+
};
|
|
724
|
+
var ENV_KEYS = {
|
|
725
|
+
appId: "LARK_APP_ID",
|
|
726
|
+
appSecret: "LARK_APP_SECRET",
|
|
727
|
+
verificationToken: "LARK_VERIFICATION_TOKEN",
|
|
728
|
+
encryptKey: "LARK_ENCRYPT_KEY",
|
|
729
|
+
baseUrl: "LARK_BASE_URL",
|
|
730
|
+
botOpenId: "LARK_BOT_OPEN_ID"
|
|
731
|
+
};
|
|
732
|
+
function defaultEnv() {
|
|
733
|
+
if (typeof process !== "undefined" && process.env) {
|
|
734
|
+
return process.env;
|
|
735
|
+
}
|
|
736
|
+
return {};
|
|
737
|
+
}
|
|
738
|
+
__name(defaultEnv, "defaultEnv");
|
|
739
|
+
function pick(input, envValue) {
|
|
740
|
+
return input ?? envValue;
|
|
741
|
+
}
|
|
742
|
+
__name(pick, "pick");
|
|
743
|
+
function resolveOptions(options, env = defaultEnv()) {
|
|
744
|
+
const appId = pick(options.appId, env[ENV_KEYS.appId]);
|
|
745
|
+
const appSecret = pick(options.appSecret, env[ENV_KEYS.appSecret]);
|
|
746
|
+
const verificationToken = pick(
|
|
747
|
+
options.verificationToken,
|
|
748
|
+
env[ENV_KEYS.verificationToken]
|
|
749
|
+
);
|
|
750
|
+
if (!appId) {
|
|
751
|
+
throw new LarkConfigError(
|
|
752
|
+
`eve-lark: appId is required (option \`appId\` or env \`${ENV_KEYS.appId}\`)`
|
|
753
|
+
);
|
|
754
|
+
}
|
|
755
|
+
if (!appSecret) {
|
|
756
|
+
throw new LarkConfigError(
|
|
757
|
+
`eve-lark: appSecret is required (option \`appSecret\` or env \`${ENV_KEYS.appSecret}\`)`
|
|
758
|
+
);
|
|
759
|
+
}
|
|
760
|
+
if (!verificationToken) {
|
|
761
|
+
throw new LarkConfigError(
|
|
762
|
+
`eve-lark: verificationToken is required (option \`verificationToken\` or env \`${ENV_KEYS.verificationToken}\`)`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
const rawBaseUrl = pick(options.baseUrl, env[ENV_KEYS.baseUrl]) ?? DEFAULTS.baseUrl;
|
|
766
|
+
const baseUrl = rawBaseUrl.replace(/\/+$/, "");
|
|
767
|
+
return {
|
|
768
|
+
appId,
|
|
769
|
+
appSecret,
|
|
770
|
+
verificationToken,
|
|
771
|
+
encryptKey: pick(options.encryptKey, env[ENV_KEYS.encryptKey]),
|
|
772
|
+
baseUrl,
|
|
773
|
+
botOpenId: pick(options.botOpenId, env[ENV_KEYS.botOpenId]),
|
|
774
|
+
webhookPath: options.webhookPath ?? DEFAULTS.webhookPath,
|
|
775
|
+
replyMode: options.replyMode ?? DEFAULTS.replyMode,
|
|
776
|
+
streamPatchIntervalMs: options.streamPatchIntervalMs ?? DEFAULTS.streamPatchIntervalMs,
|
|
777
|
+
streamCreateThresholdMs: options.streamCreateThresholdMs ?? DEFAULTS.streamCreateThresholdMs,
|
|
778
|
+
dedupTtlMs: options.dedupTtlMs ?? DEFAULTS.dedupTtlMs,
|
|
779
|
+
dedupMaxEntries: options.dedupMaxEntries ?? DEFAULTS.dedupMaxEntries,
|
|
780
|
+
requestTimeoutMs: options.requestTimeoutMs ?? DEFAULTS.requestTimeoutMs,
|
|
781
|
+
maxRetries: options.maxRetries ?? DEFAULTS.maxRetries,
|
|
782
|
+
tokenRefreshBufferMs: options.tokenRefreshBufferMs ?? DEFAULTS.tokenRefreshBufferMs,
|
|
783
|
+
signatureSkewMs: options.signatureSkewMs ?? DEFAULTS.signatureSkewMs,
|
|
784
|
+
fetch: options.fetch ?? globalThis.fetch,
|
|
785
|
+
ackReaction: options.ackReaction ?? DEFAULTS.ackReaction
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
__name(resolveOptions, "resolveOptions");
|
|
789
|
+
|
|
790
|
+
// src/channel.ts
|
|
791
|
+
function larkContinuationToken(chatId, rootMessageId) {
|
|
792
|
+
return `${chatId}:${rootMessageId ?? "_"}`;
|
|
793
|
+
}
|
|
794
|
+
__name(larkContinuationToken, "larkContinuationToken");
|
|
795
|
+
function metaFromCtx(ctx) {
|
|
796
|
+
const attrs = ctx.session?.auth?.initiator?.attributes ?? {};
|
|
797
|
+
if (typeof attrs.chatId !== "string" || !attrs.chatId) return null;
|
|
798
|
+
return {
|
|
799
|
+
chatId: attrs.chatId,
|
|
800
|
+
rootId: typeof attrs.rootMessageId === "string" ? attrs.rootMessageId : void 0,
|
|
801
|
+
parentId: typeof attrs.parentId === "string" ? attrs.parentId : void 0
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
__name(metaFromCtx, "metaFromCtx");
|
|
805
|
+
function ackOk() {
|
|
806
|
+
return Response.json({ code: 0 });
|
|
807
|
+
}
|
|
808
|
+
__name(ackOk, "ackOk");
|
|
809
|
+
function pickAckEmoji(reaction) {
|
|
810
|
+
if (typeof reaction === "string") return reaction;
|
|
811
|
+
if (Array.isArray(reaction)) {
|
|
812
|
+
if (reaction.length === 0) return false;
|
|
813
|
+
const idx = Math.floor(Math.random() * reaction.length);
|
|
814
|
+
return reaction[idx] ?? false;
|
|
815
|
+
}
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
__name(pickAckEmoji, "pickAckEmoji");
|
|
819
|
+
function resourceUrl(options, file, messageId) {
|
|
820
|
+
const type = file.kind === "image" ? "image" : "file";
|
|
821
|
+
return `${options.baseUrl}/open-apis/im/v1/messages/${encodeURIComponent(messageId)}/resources/${encodeURIComponent(file.fileKey)}?type=${type}`;
|
|
822
|
+
}
|
|
823
|
+
__name(resourceUrl, "resourceUrl");
|
|
824
|
+
function buildUserContent(text, files, options, messageId) {
|
|
825
|
+
const parts = [];
|
|
826
|
+
if (text.length > 0) parts.push({ type: "text", text });
|
|
827
|
+
for (const f of files) {
|
|
828
|
+
parts.push({
|
|
829
|
+
type: "file",
|
|
830
|
+
data: new URL(resourceUrl(options, f, messageId)),
|
|
831
|
+
mediaType: f.mediaType
|
|
832
|
+
});
|
|
833
|
+
}
|
|
834
|
+
return parts;
|
|
835
|
+
}
|
|
836
|
+
__name(buildUserContent, "buildUserContent");
|
|
837
|
+
function createLarkChannel(optionsInput) {
|
|
838
|
+
const options = resolveOptions(optionsInput);
|
|
839
|
+
const client = new LarkClient(options);
|
|
840
|
+
const dedup = new DedupMap(options.dedupTtlMs, options.dedupMaxEntries);
|
|
841
|
+
const controllers = /* @__PURE__ */ new Map();
|
|
842
|
+
const sessionMeta = /* @__PURE__ */ new Map();
|
|
843
|
+
function getController(sessionId, meta) {
|
|
844
|
+
let ctrl = controllers.get(sessionId);
|
|
845
|
+
if (!ctrl) {
|
|
846
|
+
ctrl = new StreamingCardController(client, {
|
|
847
|
+
chatId: meta.chatId,
|
|
848
|
+
rootId: meta.rootId,
|
|
849
|
+
parentId: meta.parentId,
|
|
850
|
+
patchIntervalMs: options.streamPatchIntervalMs,
|
|
851
|
+
createThresholdMs: options.streamCreateThresholdMs
|
|
852
|
+
});
|
|
853
|
+
controllers.set(sessionId, ctrl);
|
|
854
|
+
}
|
|
855
|
+
return ctrl;
|
|
856
|
+
}
|
|
857
|
+
__name(getController, "getController");
|
|
858
|
+
function lookupMeta(sessionId) {
|
|
859
|
+
return sessionMeta.get(sessionId);
|
|
860
|
+
}
|
|
861
|
+
__name(lookupMeta, "lookupMeta");
|
|
862
|
+
function dropController(sessionId) {
|
|
863
|
+
controllers.delete(sessionId);
|
|
864
|
+
sessionMeta.delete(sessionId);
|
|
865
|
+
}
|
|
866
|
+
__name(dropController, "dropController");
|
|
867
|
+
const webhookHandler = /* @__PURE__ */ __name(async (req, helpers) => {
|
|
868
|
+
const rawBody = Buffer.from(await req.arrayBuffer());
|
|
869
|
+
const tsHeader = req.headers.get("x-lark-request-timestamp") ?? "";
|
|
870
|
+
const ts = Number(tsHeader);
|
|
871
|
+
if (tsHeader && Number.isFinite(ts) && ts > 0 && Math.abs(Date.now() / 1e3 - ts) > options.signatureSkewMs / 1e3) {
|
|
872
|
+
return new Response("request timestamp out of skew window", { status: 408 });
|
|
873
|
+
}
|
|
874
|
+
let workingBody = rawBody;
|
|
875
|
+
if (options.encryptKey) {
|
|
876
|
+
const nonce = req.headers.get("x-lark-request-nonce") ?? "";
|
|
877
|
+
const sigHeader = req.headers.get("x-lark-signature");
|
|
878
|
+
if (!sigHeader) return new Response("missing signature", { status: 401 });
|
|
879
|
+
const ok = verifySignature({
|
|
880
|
+
timestamp: tsHeader,
|
|
881
|
+
nonce,
|
|
882
|
+
encryptKey: options.encryptKey,
|
|
883
|
+
rawBody,
|
|
884
|
+
signatureHeader: sigHeader
|
|
885
|
+
});
|
|
886
|
+
if (!ok) return new Response("bad signature", { status: 401 });
|
|
887
|
+
try {
|
|
888
|
+
const envelope = JSON.parse(rawBody.toString("utf8"));
|
|
889
|
+
if (envelope.encrypt) {
|
|
890
|
+
workingBody = decryptPayload(envelope.encrypt, options.encryptKey);
|
|
891
|
+
}
|
|
892
|
+
} catch {
|
|
893
|
+
return new Response("decrypt failed", { status: 400 });
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
let body;
|
|
897
|
+
try {
|
|
898
|
+
body = JSON.parse(workingBody.toString("utf8"));
|
|
899
|
+
} catch {
|
|
900
|
+
return new Response("invalid json", { status: 400 });
|
|
901
|
+
}
|
|
902
|
+
if (body.type === "url_verification") {
|
|
903
|
+
return Response.json({ challenge: body.challenge ?? "" });
|
|
904
|
+
}
|
|
905
|
+
if (body.schema !== "2.0") {
|
|
906
|
+
return ackOk();
|
|
907
|
+
}
|
|
908
|
+
if (body.header?.token !== options.verificationToken) {
|
|
909
|
+
return new Response("verification token mismatch", { status: 401 });
|
|
910
|
+
}
|
|
911
|
+
const dedupKey = body.header?.event_id ?? body.event?.message?.message_id;
|
|
912
|
+
if (dedupKey) {
|
|
913
|
+
if (dedup.has(dedupKey)) return ackOk();
|
|
914
|
+
dedup.set(dedupKey);
|
|
915
|
+
}
|
|
916
|
+
if (body.header?.event_type !== "im.message.receive_v1") {
|
|
917
|
+
return ackOk();
|
|
918
|
+
}
|
|
919
|
+
if (!body.event) return ackOk();
|
|
920
|
+
const parsed = parseInbound(body.event, options.botOpenId);
|
|
921
|
+
if (parsed.senderType === "app") {
|
|
922
|
+
return ackOk();
|
|
923
|
+
}
|
|
924
|
+
if (parsed.text === "" && parsed.files.length === 0) {
|
|
925
|
+
return ackOk();
|
|
926
|
+
}
|
|
927
|
+
const userContent = buildUserContent(parsed.text, parsed.files, options, parsed.messageId);
|
|
928
|
+
const continuationToken = larkContinuationToken(parsed.chatId, parsed.parentId ?? parsed.rootId);
|
|
929
|
+
const auth = {
|
|
930
|
+
authenticator: "lark",
|
|
931
|
+
principalType: "user",
|
|
932
|
+
principalId: parsed.senderOpenId,
|
|
933
|
+
attributes: {
|
|
934
|
+
chatId: parsed.chatId,
|
|
935
|
+
rootMessageId: parsed.rootId,
|
|
936
|
+
messageId: parsed.messageId,
|
|
937
|
+
chatType: parsed.chatType
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
const session = await helpers.send(userContent, {
|
|
941
|
+
auth,
|
|
942
|
+
continuationToken
|
|
943
|
+
});
|
|
944
|
+
sessionMeta.set(session.id, {
|
|
945
|
+
chatId: parsed.chatId,
|
|
946
|
+
rootId: parsed.rootId ?? void 0,
|
|
947
|
+
parentId: parsed.parentId ?? void 0
|
|
948
|
+
});
|
|
949
|
+
console.log("[eve-lark] session registered", session.id, "chat:", parsed.chatId);
|
|
950
|
+
const emoji = pickAckEmoji(options.ackReaction);
|
|
951
|
+
if (emoji) {
|
|
952
|
+
helpers.waitUntil(
|
|
953
|
+
client.addReaction({ messageId: parsed.messageId, emojiType: emoji }).catch((e) => {
|
|
954
|
+
console.warn("[eve-lark] ack reaction failed", e);
|
|
955
|
+
})
|
|
956
|
+
);
|
|
957
|
+
}
|
|
958
|
+
return ackOk();
|
|
959
|
+
}, "webhookHandler");
|
|
960
|
+
return defineChannel({
|
|
961
|
+
routes: [POST(options.webhookPath, webhookHandler)],
|
|
962
|
+
fetchFile: /* @__PURE__ */ __name(async (url) => {
|
|
963
|
+
if (!url.startsWith(options.baseUrl)) return null;
|
|
964
|
+
const m = url.match(/\/messages\/([^/]+)\/resources\/([^?]+)\?type=(image|file)/);
|
|
965
|
+
if (!m || !m[1] || !m[2] || !m[3]) return null;
|
|
966
|
+
return client.downloadResource({
|
|
967
|
+
messageId: m[1],
|
|
968
|
+
fileKey: m[2],
|
|
969
|
+
type: m[3]
|
|
970
|
+
});
|
|
971
|
+
}, "fetchFile"),
|
|
972
|
+
events: {
|
|
973
|
+
// Streaming delta — patch the card.
|
|
974
|
+
"message.appended"(data, _channel, ctx) {
|
|
975
|
+
console.log("[eve-lark] message.appended", ctx.session.id);
|
|
976
|
+
if (options.replyMode !== "streaming") return;
|
|
977
|
+
const sessionId = ctx.session.id;
|
|
978
|
+
const meta = metaFromCtx(ctx);
|
|
979
|
+
if (!meta) {
|
|
980
|
+
console.log("[eve-lark] message.appended: no meta for", sessionId);
|
|
981
|
+
return;
|
|
982
|
+
}
|
|
983
|
+
const d = data;
|
|
984
|
+
const ctrl = getController(sessionId, meta);
|
|
985
|
+
if (typeof d.messageDelta === "string") {
|
|
986
|
+
ctrl.appendDelta(d.messageDelta);
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
// Terminal — finalize the card OR deliver a fresh one in static mode.
|
|
990
|
+
async "message.completed"(data, _channel, ctx) {
|
|
991
|
+
console.log("[eve-lark] message.completed", ctx.session.id);
|
|
992
|
+
const sessionId = ctx.session.id;
|
|
993
|
+
const meta = metaFromCtx(ctx);
|
|
994
|
+
if (!meta) {
|
|
995
|
+
console.log("[eve-lark] message.completed: no meta for", sessionId);
|
|
996
|
+
return;
|
|
997
|
+
}
|
|
998
|
+
const d = data;
|
|
999
|
+
const text = typeof d.message === "string" ? d.message : "";
|
|
1000
|
+
if (options.replyMode === "streaming") {
|
|
1001
|
+
const ctrl = getController(sessionId, meta);
|
|
1002
|
+
try {
|
|
1003
|
+
await ctrl.finalize(text);
|
|
1004
|
+
console.log("[eve-lark] finalize done for", sessionId);
|
|
1005
|
+
} catch (e) {
|
|
1006
|
+
console.log("[eve-lark] finalize failed for", sessionId, e);
|
|
1007
|
+
}
|
|
1008
|
+
dropController(sessionId);
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
try {
|
|
1012
|
+
await client.sendCard({
|
|
1013
|
+
chatId: meta.chatId,
|
|
1014
|
+
card: buildTextCard(text),
|
|
1015
|
+
rootId: meta.rootId,
|
|
1016
|
+
parentId: meta.parentId
|
|
1017
|
+
});
|
|
1018
|
+
console.log("[eve-lark] static sendCard done for", sessionId);
|
|
1019
|
+
} catch (e) {
|
|
1020
|
+
console.log("[eve-lark] static sendCard failed:", e);
|
|
1021
|
+
await client.sendText({
|
|
1022
|
+
chatId: meta.chatId,
|
|
1023
|
+
content: text,
|
|
1024
|
+
rootId: meta.rootId,
|
|
1025
|
+
parentId: meta.parentId
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
dropController(sessionId);
|
|
1029
|
+
},
|
|
1030
|
+
async "turn.failed"(data, _channel, ctx) {
|
|
1031
|
+
console.log("[eve-lark] turn.failed", ctx?.session?.id);
|
|
1032
|
+
const sessionId = ctx?.session?.id;
|
|
1033
|
+
if (!sessionId) return;
|
|
1034
|
+
const meta = metaFromCtx(ctx);
|
|
1035
|
+
if (!meta) return;
|
|
1036
|
+
const d = data;
|
|
1037
|
+
const errMsg = typeof d === "object" && d !== null && "error" in d ? typeof d.error === "string" ? d.error : d.error?.message ?? "turn failed" : "turn failed";
|
|
1038
|
+
if (options.replyMode === "streaming") {
|
|
1039
|
+
const ctrl = controllers.get(sessionId);
|
|
1040
|
+
if (ctrl) {
|
|
1041
|
+
try {
|
|
1042
|
+
await ctrl.abort(errMsg);
|
|
1043
|
+
} catch {
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
dropController(sessionId);
|
|
1048
|
+
},
|
|
1049
|
+
async "session.failed"(data, _channel) {
|
|
1050
|
+
const d = data;
|
|
1051
|
+
const errMsg = d?.error?.message ?? "session failed";
|
|
1052
|
+
for (const [, ctrl] of controllers) {
|
|
1053
|
+
try {
|
|
1054
|
+
await ctrl.abort(errMsg);
|
|
1055
|
+
} catch {
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
1058
|
+
controllers.clear();
|
|
1059
|
+
sessionMeta.clear();
|
|
1060
|
+
}
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
__name(createLarkChannel, "createLarkChannel");
|
|
1065
|
+
export {
|
|
1066
|
+
LarkApiError,
|
|
1067
|
+
LarkChannelError,
|
|
1068
|
+
LarkClient,
|
|
1069
|
+
LarkConfigError,
|
|
1070
|
+
LarkDecryptError,
|
|
1071
|
+
LarkSignatureError,
|
|
1072
|
+
createLarkChannel,
|
|
1073
|
+
larkContinuationToken
|
|
1074
|
+
};
|
|
1075
|
+
//# sourceMappingURL=index.js.map
|