@larksuiteoapi/node-sdk 1.61.0 → 1.62.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/es/index.js CHANGED
@@ -11,14 +11,19 @@
11
11
  */
12
12
 
13
13
  import axios, { AxiosError } from 'axios';
14
+ import fs from 'fs';
15
+ import path from 'path';
14
16
  import crypto from 'crypto';
15
17
  import { stringify } from 'qs';
16
18
  import identity from 'lodash.identity';
17
19
  import pickBy from 'lodash.pickby';
18
- import fs from 'fs';
19
20
  import merge from 'lodash.merge';
20
21
  import qs from 'querystring';
21
22
  import WebSocket from 'ws';
23
+ import http from 'http';
24
+ import https from 'https';
25
+ import { promises } from 'dns';
26
+ import { isIP } from 'net';
22
27
 
23
28
  /******************************************************************************
24
29
  Copyright (c) Microsoft Corporation.
@@ -98,10 +103,72 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
98
103
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
99
104
  };
100
105
 
106
+ /**
107
+ * Resolve the SDK's own version from `package.json`. The source lives at
108
+ * `utils/user-agent.ts`; the published bundle lives at `lib/index.js` or
109
+ * `es/index.js`. In both shapes the package.json sits exactly one level
110
+ * above the code, so `__dirname/..` is the primary candidate. A second
111
+ * candidate (two levels up) covers downstream bundlers that re-emit this
112
+ * module at a deeper path.
113
+ *
114
+ * We sanity-check `name === '@larksuiteoapi/node-sdk'` so we don't
115
+ * accidentally read an unrelated package.json that happens to sit in a
116
+ * parent directory during local development.
117
+ */
118
+ let cachedVersion;
119
+ function getSdkVersion() {
120
+ if (cachedVersion !== undefined)
121
+ return cachedVersion;
122
+ const candidates = [
123
+ path.resolve(__dirname, '..', 'package.json'),
124
+ path.resolve(__dirname, '..', '..', 'package.json'),
125
+ ];
126
+ for (const p of candidates) {
127
+ try {
128
+ const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
129
+ if (pkg.name === '@larksuiteoapi/node-sdk' && typeof pkg.version === 'string') {
130
+ cachedVersion = pkg.version;
131
+ return pkg.version;
132
+ }
133
+ }
134
+ catch (_a) {
135
+ // try next candidate
136
+ }
137
+ }
138
+ cachedVersion = 'unknown';
139
+ return 'unknown';
140
+ }
141
+ /**
142
+ * Sanitize a caller-supplied `source` tag so it can be safely concatenated
143
+ * into a User-Agent header. Non-token characters are replaced with '_'
144
+ * and the result is clamped to 64 characters. Never throws.
145
+ */
146
+ function sanitizeSource(raw) {
147
+ return raw.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 64);
148
+ }
149
+ /**
150
+ * Build the User-Agent value. When `source` is provided it appends a
151
+ * `source/<sanitized>` product token, e.g.
152
+ * oapi-node-sdk/1.62.0 source/cursor-bot
153
+ * Falsy or all-invalid-char `source` values produce the base UA without
154
+ * the extra token.
155
+ */
156
+ function buildUserAgent(source) {
157
+ const base = `oapi-node-sdk/${getSdkVersion()}`;
158
+ if (!source)
159
+ return base;
160
+ const clean = sanitizeSource(source);
161
+ return clean ? `${base} source/${clean}` : base;
162
+ }
163
+
101
164
  const defaultHttpInstance = axios.create();
165
+ // Fallback UA for callers that bypass Client/WSClient and hit
166
+ // `defaultHttpInstance` directly. Client.formatPayload overrides this with
167
+ // a source-enriched UA when a `source` option is configured.
168
+ const FALLBACK_UA = buildUserAgent();
102
169
  defaultHttpInstance.interceptors.request.use((req) => {
103
- if (req.headers) {
104
- req.headers['User-Agent'] = 'oapi-node-sdk/1.0.0';
170
+ if (req.headers && !req.headers['User-Agent']) {
171
+ req.headers['User-Agent'] = FALLBACK_UA;
105
172
  }
106
173
  return req;
107
174
  }, undefined, { synchronous: true });
@@ -83926,6 +83993,7 @@ class Client extends Client$1 {
83926
83993
  this.appId = params.appId;
83927
83994
  this.appSecret = params.appSecret;
83928
83995
  this.disableTokenCache = params.disableTokenCache;
83996
+ this.userAgent = buildUserAgent(params.source);
83929
83997
  assert(!this.appId, () => this.logger.error('appId is needed'));
83930
83998
  assert(!this.appSecret, () => this.logger.error('appSecret is needed'));
83931
83999
  this.helpDeskId = params.helpDeskId;
@@ -83985,7 +84053,7 @@ class Client extends Client$1 {
83985
84053
  }
83986
84054
  return {
83987
84055
  params: Object.assign(Object.assign({}, ((payload === null || payload === void 0 ? void 0 : payload.params) || {})), targetOptions.params),
83988
- headers: Object.assign(Object.assign({ 'User-Agent': 'oapi-node-sdk/1.0.0' }, ((payload === null || payload === void 0 ? void 0 : payload.headers) || {})), targetOptions.headers),
84056
+ headers: Object.assign(Object.assign({ 'User-Agent': this.userAgent }, ((payload === null || payload === void 0 ? void 0 : payload.headers) || {})), targetOptions.headers),
83989
84057
  data: Object.assign(Object.assign({}, ((payload === null || payload === void 0 ? void 0 : payload.data) || {})), targetOptions.data),
83990
84058
  path: Object.assign(Object.assign({}, ((payload === null || payload === void 0 ? void 0 : payload.path) || {})), targetOptions.path),
83991
84059
  };
@@ -85291,7 +85359,11 @@ class WSClient {
85291
85359
  lastConnectTime: 0,
85292
85360
  nextConnectTime: 0,
85293
85361
  };
85294
- const { appId, appSecret, agent, domain = Domain.Feishu, httpInstance = defaultHttpInstance, loggerLevel = LoggerLevel.info, logger = defaultLogger, autoReconnect = true } = params;
85362
+ /** True if the WS has ever connected successfully in this client's
85363
+ * lifetime — used to distinguish first-connect from reconnect. */
85364
+ this.hasEverConnected = false;
85365
+ const { appId, appSecret, agent, domain = Domain.Feishu, httpInstance = defaultHttpInstance, loggerLevel = LoggerLevel.info, logger = defaultLogger, autoReconnect = true, source, onReady, onError, onReconnecting, onReconnected, } = params;
85366
+ this.userAgent = buildUserAgent(source);
85295
85367
  this.logger = new LoggerProxy(loggerLevel, logger);
85296
85368
  assert(!appId, () => this.logger.error('appId is needed'));
85297
85369
  assert(!appSecret, () => this.logger.error('appSecret is needed'));
@@ -85306,6 +85378,24 @@ class WSClient {
85306
85378
  this.wsConfig.updateWs({
85307
85379
  autoReconnect
85308
85380
  });
85381
+ this.onReady = onReady;
85382
+ this.onError = onError;
85383
+ this.onReconnecting = onReconnecting;
85384
+ this.onReconnected = onReconnected;
85385
+ }
85386
+ /**
85387
+ * Invoke a user-supplied callback safely: no-op if undefined, swallow any
85388
+ * exception to avoid breaking the WS state machine.
85389
+ */
85390
+ safeInvoke(label, fn, ...args) {
85391
+ if (!fn)
85392
+ return;
85393
+ try {
85394
+ fn(...args);
85395
+ }
85396
+ catch (e) {
85397
+ this.logger.error(`[ws] ${label} callback threw`, e);
85398
+ }
85309
85399
  }
85310
85400
  pullConnectConfig() {
85311
85401
  return __awaiter(this, void 0, void 0, function* () {
@@ -85321,6 +85411,7 @@ class WSClient {
85321
85411
  // consumed by gateway
85322
85412
  headers: {
85323
85413
  "locale": "zh",
85414
+ "User-Agent": this.userAgent,
85324
85415
  },
85325
85416
  timeout: 15000,
85326
85417
  });
@@ -85414,7 +85505,11 @@ class WSClient {
85414
85505
  finally {
85415
85506
  this.isConnecting = false;
85416
85507
  }
85417
- if (!isSuccess) {
85508
+ if (isSuccess) {
85509
+ this.hasEverConnected = true;
85510
+ this.safeInvoke('onReady', this.onReady);
85511
+ }
85512
+ else {
85418
85513
  this.logger.error('[ws]', 'connect failed');
85419
85514
  yield this.reConnect();
85420
85515
  }
@@ -85423,9 +85518,15 @@ class WSClient {
85423
85518
  }
85424
85519
  const { autoReconnect, reconnectNonce, reconnectCount, reconnectInterval } = this.wsConfig.getWS();
85425
85520
  if (!autoReconnect) {
85521
+ if (!this.hasEverConnected) {
85522
+ this.safeInvoke('onError', this.onError, new Error('WebSocket connect failed and autoReconnect is disabled'));
85523
+ }
85426
85524
  return;
85427
85525
  }
85428
85526
  this.logger.info('[ws]', 'reconnect');
85527
+ if (this.hasEverConnected) {
85528
+ this.safeInvoke('onReconnecting', this.onReconnecting);
85529
+ }
85429
85530
  if (wsInstance) {
85430
85531
  wsInstance === null || wsInstance === void 0 ? void 0 : wsInstance.terminate();
85431
85532
  }
@@ -85447,12 +85548,20 @@ class WSClient {
85447
85548
  // if reconnectCount < 0, the reconnect time is infinite
85448
85549
  if (isSuccess) {
85449
85550
  this.logger.debug('[ws]', 'reconnect success');
85551
+ if (this.hasEverConnected) {
85552
+ this.safeInvoke('onReconnected', this.onReconnected);
85553
+ }
85554
+ else {
85555
+ this.hasEverConnected = true;
85556
+ this.safeInvoke('onReady', this.onReady);
85557
+ }
85450
85558
  this.isConnecting = false;
85451
85559
  return;
85452
85560
  }
85453
85561
  this.logger.info('ws', `unable to connect to the server after trying ${count} times")`);
85454
85562
  if (reconnectCount >= 0 && count >= reconnectCount) {
85455
85563
  this.isConnecting = false;
85564
+ this.safeInvoke('onError', this.onError, new Error(`WebSocket reconnect exhausted after ${count} attempts`));
85456
85565
  return;
85457
85566
  }
85458
85567
  this.reconnectInterval = setTimeout(() => {
@@ -86029,13 +86138,13 @@ function registerApp(options) {
86029
86138
  qrCodeUrl.searchParams.set('tp', 'sdk');
86030
86139
  onQRCodeReady({
86031
86140
  url: qrCodeUrl.toString(),
86032
- expireIn: (_a = beginRes.expire_in) !== null && _a !== void 0 ? _a : 600,
86141
+ expireIn: (_a = beginRes.expires_in) !== null && _a !== void 0 ? _a : 600,
86033
86142
  });
86034
86143
  return startPolling({
86035
86144
  baseUrl,
86036
86145
  deviceCode: beginRes.device_code,
86037
86146
  interval: ((_b = beginRes.interval) !== null && _b !== void 0 ? _b : 5) * 1000,
86038
- expireIn: ((_c = beginRes.expire_in) !== null && _c !== void 0 ? _c : 600) * 1000,
86147
+ expireIn: ((_c = beginRes.expires_in) !== null && _c !== void 0 ? _c : 600) * 1000,
86039
86148
  larkBaseUrl: `https://${(_d = options.larkDomain) !== null && _d !== void 0 ? _d : DEFAULT_LARK_DOMAIN}`,
86040
86149
  signal,
86041
86150
  onStatusChange,
@@ -86043,4 +86152,3652 @@ function registerApp(options) {
86043
86152
  });
86044
86153
  }
86045
86154
 
86046
- export { AESCipher, Aily, AppType, CAppTicket, CTenantAccessToken, CardActionHandler, Client, Domain, EventDispatcher, LoggerLevel, WSClient, adaptDefault, adaptExpress, adaptKoa, adaptKoaRouter, defaultHttpInstance, generateChallenge, messageCard, registerApp, withAll, withHelpDeskCredential, withTenantKey, withTenantToken, withUserAccessToken };
86155
+ class LarkChannelError extends Error {
86156
+ constructor(code, message, opts) {
86157
+ super(message);
86158
+ this.name = 'LarkChannelError';
86159
+ this.code = code;
86160
+ this.cause = opts === null || opts === void 0 ? void 0 : opts.cause;
86161
+ this.context = opts === null || opts === void 0 ? void 0 : opts.context;
86162
+ }
86163
+ }
86164
+
86165
+ /**
86166
+ * Dual-threshold throttle: fires `flush` either when `ms` have elapsed
86167
+ * since the last fire OR when `chars` characters have accumulated.
86168
+ *
86169
+ * Usage inside a stream controller:
86170
+ * const t = new Throttle({ ms: 100, chars: 50 }, () => doPatch(buffer));
86171
+ * await t.note(deltaLen); // may or may not fire, schedule the rest
86172
+ * await t.flushNow(); // end-of-stream force flush
86173
+ */
86174
+ class Throttle {
86175
+ constructor(opts, fire) {
86176
+ this.opts = opts;
86177
+ this.fire = fire;
86178
+ this.pendingChars = 0;
86179
+ this.lastFireAt = 0;
86180
+ }
86181
+ /**
86182
+ * Accumulate bytes and decide whether to fire now, schedule a timer,
86183
+ * or do nothing (a fire is already scheduled).
86184
+ */
86185
+ note(deltaChars) {
86186
+ this.pendingChars += deltaChars;
86187
+ if (this.pendingChars >= this.opts.chars) {
86188
+ this.fireSoon(0);
86189
+ return;
86190
+ }
86191
+ if (!this.timer) {
86192
+ const elapsed = Date.now() - this.lastFireAt;
86193
+ const wait = Math.max(0, this.opts.ms - elapsed);
86194
+ this.fireSoon(wait);
86195
+ }
86196
+ }
86197
+ fireSoon(delay) {
86198
+ if (this.timer)
86199
+ clearTimeout(this.timer);
86200
+ this.timer = setTimeout(() => {
86201
+ this.timer = undefined;
86202
+ void this.doFire();
86203
+ }, delay);
86204
+ }
86205
+ /**
86206
+ * Force-flush everything accumulated so far, including any content that
86207
+ * arrived during an in-flight fire. Waits for the in-flight fire to
86208
+ * complete, then fires once more to capture the final state — without
86209
+ * this second fire, the last chunk of appended content would be missed
86210
+ * (the in-flight fire captured a snapshot BEFORE those chunks arrived).
86211
+ */
86212
+ flushNow() {
86213
+ return __awaiter(this, void 0, void 0, function* () {
86214
+ if (this.timer) {
86215
+ clearTimeout(this.timer);
86216
+ this.timer = undefined;
86217
+ }
86218
+ if (this.inFlight) {
86219
+ yield this.inFlight;
86220
+ }
86221
+ yield this.doFire();
86222
+ });
86223
+ }
86224
+ doFire() {
86225
+ return __awaiter(this, void 0, void 0, function* () {
86226
+ if (this.inFlight) {
86227
+ // Someone else is firing; schedule a follow-up so the latest
86228
+ // accumulated chars get captured after the in-flight fire ends.
86229
+ this.fireSoon(this.opts.ms);
86230
+ return;
86231
+ }
86232
+ const p = (() => __awaiter(this, void 0, void 0, function* () {
86233
+ this.pendingChars = 0;
86234
+ this.lastFireAt = Date.now();
86235
+ yield this.fire();
86236
+ }))();
86237
+ this.inFlight = p;
86238
+ try {
86239
+ yield p;
86240
+ }
86241
+ finally {
86242
+ this.inFlight = undefined;
86243
+ }
86244
+ });
86245
+ }
86246
+ dispose() {
86247
+ if (this.timer) {
86248
+ clearTimeout(this.timer);
86249
+ this.timer = undefined;
86250
+ }
86251
+ }
86252
+ }
86253
+
86254
+ /**
86255
+ * A per-stream FIFO queue that serializes async updates, so that concurrent
86256
+ * calls to `append()` / `update()` always result in PATCH operations
86257
+ * happening in submission order.
86258
+ */
86259
+ class UpdateQueue {
86260
+ constructor() {
86261
+ this.tail = Promise.resolve();
86262
+ }
86263
+ enqueue(task) {
86264
+ const next = this.tail.then(task, task);
86265
+ this.tail = next.then(() => undefined, () => undefined);
86266
+ return next;
86267
+ }
86268
+ drain() {
86269
+ return this.tail;
86270
+ }
86271
+ }
86272
+
86273
+ /**
86274
+ * Merge a streaming text chunk into an accumulator, handling both "delta"
86275
+ * and "accumulated" producers transparently.
86276
+ *
86277
+ * Rules:
86278
+ * - If `next` starts with `prev`, it's an accumulated stream → use `next`
86279
+ * - If `prev` starts with `next`, the stream rewound or overlapped → keep `prev`
86280
+ * - Otherwise, find the longest overlap between `prev`'s tail and `next`'s
86281
+ * head, and concatenate
86282
+ */
86283
+ function mergeStreamingText(prev, next) {
86284
+ if (!prev)
86285
+ return next;
86286
+ if (!next)
86287
+ return prev;
86288
+ if (next.startsWith(prev))
86289
+ return next;
86290
+ if (prev.startsWith(next))
86291
+ return prev;
86292
+ const maxOverlap = Math.min(prev.length, next.length);
86293
+ for (let len = maxOverlap; len > 0; len--) {
86294
+ if (prev.endsWith(next.substring(0, len))) {
86295
+ return prev + next.substring(len);
86296
+ }
86297
+ }
86298
+ return prev + next;
86299
+ }
86300
+
86301
+ const DEFAULT_THROTTLE_MS$1 = 100;
86302
+ const DEFAULT_THROTTLE_CHARS$1 = 50;
86303
+ const DEFAULT_INITIAL = 'Thinking...';
86304
+ const DEFAULT_EMPTY = '(no content)';
86305
+ const INITIAL_SUMMARY = '[Generating...]';
86306
+ const SUMMARY_MAX_CHARS = 50;
86307
+ const ERROR_FOOTER = '\n\n— _(Generation interrupted)_';
86308
+ const ELEMENT_ID = 'stream_md';
86309
+ /**
86310
+ * Shorten a markdown content string into a single-line preview suitable for
86311
+ * the card's `summary.content` — shown in chat lists / message previews.
86312
+ */
86313
+ function truncateSummary(text, max = SUMMARY_MAX_CHARS) {
86314
+ if (!text)
86315
+ return '';
86316
+ const cleaned = text.replace(/\s+/g, ' ').trim();
86317
+ return cleaned.length <= max ? cleaned : cleaned.slice(0, max - 1) + '…';
86318
+ }
86319
+ /**
86320
+ * Build the initial card JSON for a streaming markdown reply.
86321
+ *
86322
+ * `streaming_mode: true` tells Feishu client to render incremental updates
86323
+ * (via the cardElement.content API) as a native typewriter animation.
86324
+ *
86325
+ * `print_strategy: 'fast'` means: when a new full-content update arrives,
86326
+ * immediately show any already-buffered-but-not-yet-animated text, so the
86327
+ * display doesn't lag behind the upstream token rate.
86328
+ */
86329
+ function buildStreamingCard(initialText) {
86330
+ return {
86331
+ schema: '2.0',
86332
+ config: {
86333
+ streaming_mode: true,
86334
+ summary: { content: INITIAL_SUMMARY },
86335
+ streaming_config: {
86336
+ print_frequency_ms: { default: 70 },
86337
+ print_step: { default: 1 },
86338
+ print_strategy: 'fast',
86339
+ },
86340
+ },
86341
+ body: {
86342
+ elements: [
86343
+ {
86344
+ tag: 'markdown',
86345
+ element_id: ELEMENT_ID,
86346
+ content: initialText,
86347
+ },
86348
+ ],
86349
+ },
86350
+ };
86351
+ }
86352
+ /**
86353
+ * Streaming markdown reply that uses Feishu's native typewriter effect
86354
+ * (cardkit.v1.cardElement.content).
86355
+ *
86356
+ * Flow:
86357
+ * 1. ensureStarted(): create a card instance with streaming_mode=true,
86358
+ * send it as an interactive message referencing the card_id.
86359
+ * 2. append(chunk) / setContent(full): accumulate locally, throttle.
86360
+ * 3. throttle fires: updateCardElementContent(content, sequence++)
86361
+ * Feishu client renders typewriter animation based on the diff.
86362
+ * 4. completeTerminal(): drain queue + finishStreamingCard to disable
86363
+ * streaming_mode (removes the typing cursor).
86364
+ * 5. producer throws → append ERROR_FOOTER + finish stream → rethrow.
86365
+ *
86366
+ * The controller keeps its public shape (`append / setContent / run /
86367
+ * messageId`) — callers don't see the implementation change.
86368
+ */
86369
+ class MarkdownStreamControllerImpl {
86370
+ constructor(sender, to, idType, opts) {
86371
+ var _a, _b;
86372
+ this.sender = sender;
86373
+ this.to = to;
86374
+ this.idType = idType;
86375
+ this.opts = opts;
86376
+ this.content = '';
86377
+ this._messageId = '';
86378
+ this.cardId = '';
86379
+ this.sequence = 0;
86380
+ this.queue = new UpdateQueue();
86381
+ this.started = false;
86382
+ const cfg = this.sender.config;
86383
+ this.throttle = new Throttle({
86384
+ ms: (_a = cfg.streamThrottleMs) !== null && _a !== void 0 ? _a : DEFAULT_THROTTLE_MS$1,
86385
+ chars: (_b = cfg.streamThrottleChars) !== null && _b !== void 0 ? _b : DEFAULT_THROTTLE_CHARS$1,
86386
+ }, () => this.pushContent());
86387
+ }
86388
+ get messageId() {
86389
+ return this._messageId;
86390
+ }
86391
+ append(chunk) {
86392
+ return __awaiter(this, void 0, void 0, function* () {
86393
+ if (!chunk)
86394
+ return;
86395
+ yield this.ensureStarted();
86396
+ this.content = mergeStreamingText(this.content, chunk);
86397
+ this.throttle.note(chunk.length);
86398
+ });
86399
+ }
86400
+ setContent(full) {
86401
+ return __awaiter(this, void 0, void 0, function* () {
86402
+ yield this.ensureStarted();
86403
+ this.content = full !== null && full !== void 0 ? full : '';
86404
+ this.throttle.note(Number.MAX_SAFE_INTEGER);
86405
+ });
86406
+ }
86407
+ run(producer) {
86408
+ return __awaiter(this, void 0, void 0, function* () {
86409
+ // Eagerly send the placeholder card so `run()` always returns a real
86410
+ // messageId, and so that failTerminal / completeTerminal have a card
86411
+ // to PATCH even if the producer never appends anything.
86412
+ yield this.ensureStarted();
86413
+ try {
86414
+ yield producer(this);
86415
+ }
86416
+ catch (e) {
86417
+ yield this.failTerminal(e);
86418
+ throw e;
86419
+ }
86420
+ yield this.completeTerminal();
86421
+ return { messageId: this._messageId };
86422
+ });
86423
+ }
86424
+ // ─── internals ─────────────────────────────────────────
86425
+ ensureStarted() {
86426
+ var _a;
86427
+ return __awaiter(this, void 0, void 0, function* () {
86428
+ if (this.started)
86429
+ return;
86430
+ this.started = true;
86431
+ const initialText = (_a = this.sender.config.streamInitialText) !== null && _a !== void 0 ? _a : DEFAULT_INITIAL;
86432
+ const cardSpec = buildStreamingCard(initialText || '...');
86433
+ this.cardId = yield this.sender.createCardInstance(cardSpec);
86434
+ this._messageId = yield this.sender.sendCardByReference(this.to, this.idType, this.cardId, this.opts);
86435
+ });
86436
+ }
86437
+ pushContent() {
86438
+ return __awaiter(this, void 0, void 0, function* () {
86439
+ if (!this.cardId)
86440
+ return;
86441
+ const snapshot = this.content || '...';
86442
+ const seq = ++this.sequence;
86443
+ yield this.queue.enqueue(() => __awaiter(this, void 0, void 0, function* () {
86444
+ yield this.sender.updateCardElementContent(this.cardId, ELEMENT_ID, snapshot, seq);
86445
+ }));
86446
+ });
86447
+ }
86448
+ completeTerminal() {
86449
+ var _a, _b;
86450
+ return __awaiter(this, void 0, void 0, function* () {
86451
+ yield this.throttle.flushNow();
86452
+ yield this.queue.drain();
86453
+ if (!this.cardId)
86454
+ return;
86455
+ // If the producer never appended anything, PATCH the card to a
86456
+ // neutral terminal state so the user sees the response is finalized
86457
+ // rather than a stuck "Thinking..." placeholder.
86458
+ if (!this.content) {
86459
+ const seq = ++this.sequence;
86460
+ yield this.queue.enqueue(() => __awaiter(this, void 0, void 0, function* () {
86461
+ try {
86462
+ yield this.sender.updateCardElementContent(this.cardId, ELEMENT_ID, DEFAULT_EMPTY, seq);
86463
+ }
86464
+ catch (_c) {
86465
+ // best effort
86466
+ }
86467
+ }));
86468
+ yield this.queue.drain();
86469
+ }
86470
+ try {
86471
+ yield this.sender.finishStreamingCard(this.cardId, ++this.sequence, truncateSummary(this.content || DEFAULT_EMPTY));
86472
+ }
86473
+ catch (e) {
86474
+ // best effort — Feishu auto-closes after 10min anyway
86475
+ (_b = (_a = this.sender.logger).warn) === null || _b === void 0 ? void 0 : _b.call(_a, '[stream] finishStreamingCard failed', e);
86476
+ }
86477
+ });
86478
+ }
86479
+ failTerminal(_err) {
86480
+ return __awaiter(this, void 0, void 0, function* () {
86481
+ this.throttle.dispose();
86482
+ if (!this.cardId)
86483
+ return;
86484
+ this.content = (this.content || '') + ERROR_FOOTER;
86485
+ const seq = ++this.sequence;
86486
+ yield this.queue.enqueue(() => __awaiter(this, void 0, void 0, function* () {
86487
+ try {
86488
+ yield this.sender.updateCardElementContent(this.cardId, ELEMENT_ID, this.content, seq);
86489
+ }
86490
+ catch (_b) {
86491
+ // best effort
86492
+ }
86493
+ }));
86494
+ yield this.queue.drain();
86495
+ try {
86496
+ yield this.sender.finishStreamingCard(this.cardId, ++this.sequence, truncateSummary(this.content));
86497
+ }
86498
+ catch (_a) {
86499
+ // best effort
86500
+ }
86501
+ });
86502
+ }
86503
+ }
86504
+ class MarkdownStreamController {
86505
+ constructor(sender, to, idType, opts) {
86506
+ this.impl = new MarkdownStreamControllerImpl(sender, to, idType, opts);
86507
+ }
86508
+ run(producer) {
86509
+ return this.impl.run(producer);
86510
+ }
86511
+ }
86512
+
86513
+ const DEFAULT_THROTTLE_MS = 100;
86514
+ const DEFAULT_THROTTLE_CHARS = 50;
86515
+ /**
86516
+ * Streaming card reply — the caller provides an initial card JSON and a
86517
+ * producer that drives incremental `update()` calls with full or partial
86518
+ * new card state.
86519
+ *
86520
+ * Updates are throttled + serialized. On producer exception, the last
86521
+ * known state is kept and an error footer element is appended.
86522
+ */
86523
+ class CardStreamControllerImpl {
86524
+ constructor(sender, to, idType, opts, initial) {
86525
+ var _a, _b;
86526
+ this.sender = sender;
86527
+ this.to = to;
86528
+ this.idType = idType;
86529
+ this.opts = opts;
86530
+ this._messageId = '';
86531
+ this.queue = new UpdateQueue();
86532
+ this._current = initial;
86533
+ const cfg = sender.config;
86534
+ this.throttle = new Throttle({
86535
+ ms: (_a = cfg.streamThrottleMs) !== null && _a !== void 0 ? _a : DEFAULT_THROTTLE_MS,
86536
+ chars: (_b = cfg.streamThrottleChars) !== null && _b !== void 0 ? _b : DEFAULT_THROTTLE_CHARS,
86537
+ }, () => this.patch());
86538
+ }
86539
+ get messageId() {
86540
+ return this._messageId;
86541
+ }
86542
+ get current() {
86543
+ return this._current;
86544
+ }
86545
+ update(next) {
86546
+ return __awaiter(this, void 0, void 0, function* () {
86547
+ const nextCard = typeof next === 'function' ? next(this._current) : next;
86548
+ this._current = nextCard;
86549
+ this.throttle.note(JSON.stringify(nextCard).length);
86550
+ });
86551
+ }
86552
+ run(producer) {
86553
+ return __awaiter(this, void 0, void 0, function* () {
86554
+ yield this.sendInitial();
86555
+ try {
86556
+ yield producer(this);
86557
+ }
86558
+ catch (e) {
86559
+ yield this.failTerminal(e);
86560
+ throw e;
86561
+ }
86562
+ yield this.completeTerminal();
86563
+ return { messageId: this._messageId };
86564
+ });
86565
+ }
86566
+ // ─── internals ─────────────────────────────────────────
86567
+ sendInitial() {
86568
+ return __awaiter(this, void 0, void 0, function* () {
86569
+ const id = yield this.sender.sendOneWithFallback({
86570
+ to: this.to,
86571
+ idType: this.idType,
86572
+ msgType: 'interactive',
86573
+ content: this._current,
86574
+ replyTo: this.opts.replyTo,
86575
+ replyInThread: this.opts.replyInThread,
86576
+ });
86577
+ this._messageId = id;
86578
+ });
86579
+ }
86580
+ patch() {
86581
+ return __awaiter(this, void 0, void 0, function* () {
86582
+ if (!this._messageId)
86583
+ return;
86584
+ const snapshot = this._current;
86585
+ yield this.queue.enqueue(() => __awaiter(this, void 0, void 0, function* () {
86586
+ yield this.sender.patchCard(this._messageId, snapshot);
86587
+ }));
86588
+ });
86589
+ }
86590
+ completeTerminal() {
86591
+ return __awaiter(this, void 0, void 0, function* () {
86592
+ yield this.throttle.flushNow();
86593
+ yield this.queue.drain();
86594
+ });
86595
+ }
86596
+ failTerminal(_err) {
86597
+ return __awaiter(this, void 0, void 0, function* () {
86598
+ this.throttle.dispose();
86599
+ const withFooter = appendErrorFooter(this._current);
86600
+ yield this.queue.enqueue(() => __awaiter(this, void 0, void 0, function* () {
86601
+ try {
86602
+ yield this.sender.patchCard(this._messageId, withFooter);
86603
+ }
86604
+ catch (_a) {
86605
+ // best effort
86606
+ }
86607
+ }));
86608
+ yield this.queue.drain();
86609
+ });
86610
+ }
86611
+ }
86612
+ function appendErrorFooter(card) {
86613
+ const cardObj = card;
86614
+ const elements = Array.isArray(cardObj === null || cardObj === void 0 ? void 0 : cardObj.elements) ? [...cardObj.elements] : [];
86615
+ elements.push({
86616
+ tag: 'note',
86617
+ elements: [{ tag: 'plain_text', content: '⚠️ 生成中断' }],
86618
+ });
86619
+ return Object.assign(Object.assign({}, card), { elements });
86620
+ }
86621
+ class CardStreamController {
86622
+ constructor(sender, to, idType, opts, initial) {
86623
+ this.impl = new CardStreamControllerImpl(sender, to, idType, opts, initial);
86624
+ }
86625
+ run(producer) {
86626
+ return this.impl.run(producer);
86627
+ }
86628
+ }
86629
+
86630
+ /**
86631
+ * Parse Opus/OGG audio duration (ms) from a buffer.
86632
+ *
86633
+ * Scans backward for the last "OggS" page capture pattern (0x4f676753) and
86634
+ * reads its granule_position (64-bit LE at offset +6 from magic). For Opus,
86635
+ * granule_position is the number of decoded samples at 48 kHz — divide by
86636
+ * 48 (samples per ms) to get milliseconds.
86637
+ *
86638
+ * Returns undefined if the buffer doesn't contain a valid Ogg stream or if
86639
+ * the granule position is obviously invalid.
86640
+ */
86641
+ function parseOpusDuration(buf) {
86642
+ if (!buf || buf.length < 27)
86643
+ return undefined;
86644
+ for (let i = buf.length - 27; i >= 0; i--) {
86645
+ // "OggS" = 0x4f 67 67 53
86646
+ if (buf[i] === 0x4f &&
86647
+ buf[i + 1] === 0x67 &&
86648
+ buf[i + 2] === 0x67 &&
86649
+ buf[i + 3] === 0x53) {
86650
+ const granule = buf.readBigInt64LE(i + 6);
86651
+ if (granule < BigInt(0))
86652
+ return undefined;
86653
+ const ms = Number(granule) / 48;
86654
+ if (!Number.isFinite(ms) || ms < 0)
86655
+ return undefined;
86656
+ return Math.round(ms);
86657
+ }
86658
+ }
86659
+ return undefined;
86660
+ }
86661
+
86662
+ /**
86663
+ * Parse MP4 video duration (ms) from a buffer by walking the ISO BMFF box
86664
+ * hierarchy and finding `moov → mvhd`.
86665
+ *
86666
+ * The `mvhd` box version determines field widths:
86667
+ * - version 0: creation/modification/timescale/duration = 4 bytes each
86668
+ * - version 1: creation/modification = 8 bytes, timescale = 4 bytes,
86669
+ * duration = 8 bytes
86670
+ *
86671
+ * Returns undefined if the stream is not parseable or duration is unknown.
86672
+ */
86673
+ function parseMp4Duration(buf) {
86674
+ if (!buf || buf.length < 16)
86675
+ return undefined;
86676
+ const moov = findBoxPayload(buf, 0, buf.length, 'moov');
86677
+ if (!moov)
86678
+ return undefined;
86679
+ const mvhd = findBoxPayload(buf, moov.start, moov.end, 'mvhd');
86680
+ if (!mvhd)
86681
+ return undefined;
86682
+ const p = mvhd.start;
86683
+ if (p + 4 > buf.length)
86684
+ return undefined;
86685
+ const version = buf.readUInt8(p);
86686
+ // skip flags (3 bytes)
86687
+ const base = p + 4;
86688
+ let timescale;
86689
+ let duration;
86690
+ if (version === 1) {
86691
+ // creation(8) + modification(8) + timescale(4) + duration(8)
86692
+ if (base + 28 > buf.length)
86693
+ return undefined;
86694
+ timescale = buf.readUInt32BE(base + 16);
86695
+ duration = Number(buf.readBigUInt64BE(base + 20));
86696
+ }
86697
+ else {
86698
+ // creation(4) + modification(4) + timescale(4) + duration(4)
86699
+ if (base + 16 > buf.length)
86700
+ return undefined;
86701
+ timescale = buf.readUInt32BE(base + 8);
86702
+ duration = buf.readUInt32BE(base + 12);
86703
+ }
86704
+ if (!timescale || !Number.isFinite(timescale) || !Number.isFinite(duration)) {
86705
+ return undefined;
86706
+ }
86707
+ return Math.round((duration / timescale) * 1000);
86708
+ }
86709
+ /**
86710
+ * Walk the boxes from [begin, end) looking for one whose type matches
86711
+ * `name`. Returns the payload range (excluding the 8-byte header).
86712
+ */
86713
+ function findBoxPayload(buf, begin, end, name) {
86714
+ let p = begin;
86715
+ while (p + 8 <= end && p + 8 <= buf.length) {
86716
+ const size = buf.readUInt32BE(p);
86717
+ const type = buf.slice(p + 4, p + 8).toString('ascii');
86718
+ const boxEnd = size === 1
86719
+ ? p + Number(buf.readBigUInt64BE(p + 8))
86720
+ : size === 0
86721
+ ? end // box extends to end-of-file
86722
+ : p + size;
86723
+ if (boxEnd <= p || boxEnd > end)
86724
+ return undefined;
86725
+ if (type === name) {
86726
+ const payloadStart = size === 1 ? p + 16 : p + 8;
86727
+ return { start: payloadStart, end: boxEnd };
86728
+ }
86729
+ p = boxEnd;
86730
+ }
86731
+ return undefined;
86732
+ }
86733
+
86734
+ /**
86735
+ * CIDR blocks that must not be reached from a URL fetch triggered by the
86736
+ * SDK. Covers loopback, link-local, private ranges, CGNAT, documentation.
86737
+ */
86738
+ const BLOCKED_V4 = [
86739
+ [0x00000000, 8],
86740
+ [0x0a000000, 8],
86741
+ [0x7f000000, 8],
86742
+ [0xa9fe0000, 16],
86743
+ [0xac100000, 12],
86744
+ [0xc0a80000, 16],
86745
+ [0x64400000, 10],
86746
+ [0xc0000000, 24],
86747
+ [0xc0000200, 24],
86748
+ [0xc6120000, 15],
86749
+ [0xc6336400, 24],
86750
+ [0xcb007100, 24],
86751
+ [0xe0000000, 4],
86752
+ [0xf0000000, 4], // 240.0.0.0/4 (reserved)
86753
+ ];
86754
+ /**
86755
+ * Validate that `url` does not resolve to an internal / reserved IP, and
86756
+ * return the single IP that downstream fetching should pin to. Pinning is
86757
+ * essential to close the DNS-rebinding TOCTOU gap: the attacker's DNS
86758
+ * server could otherwise return a public IP to this check and a private
86759
+ * IP (e.g. cloud metadata `169.254.169.254`) to the subsequent fetch.
86760
+ *
86761
+ * Allowlisted hostnames skip the public-IP check but still pin — pinning
86762
+ * is only about consistency between check-time and use-time, and is
86763
+ * harmless when the caller trusts the host.
86764
+ */
86765
+ function assertPublicUrl(url, opts = {}) {
86766
+ var _a, _b;
86767
+ return __awaiter(this, void 0, void 0, function* () {
86768
+ const u = new URL(url);
86769
+ if (u.protocol !== 'http:' && u.protocol !== 'https:') {
86770
+ throw new Error(`ssrf_blocked: protocol ${u.protocol}`);
86771
+ }
86772
+ // URL keeps IPv6 literals wrapped in brackets (`[::1]`) — strip them so
86773
+ // `isIP()` recognizes the literal and we don't fall through to DNS.
86774
+ const rawHost = u.hostname;
86775
+ const host = rawHost.startsWith('[') && rawHost.endsWith(']')
86776
+ ? rawHost.slice(1, -1)
86777
+ : rawHost;
86778
+ const allowlisted = (_b = (_a = opts.allowlist) === null || _a === void 0 ? void 0 : _a.includes(host)) !== null && _b !== void 0 ? _b : false;
86779
+ let resolvedIp;
86780
+ if (isIP(host)) {
86781
+ resolvedIp = host;
86782
+ if (!allowlisted)
86783
+ assertIpPublic(resolvedIp);
86784
+ }
86785
+ else {
86786
+ const records = yield promises.lookup(host, { all: true });
86787
+ if (records.length === 0) {
86788
+ throw new Error(`ssrf_blocked: no DNS records for ${host}`);
86789
+ }
86790
+ if (!allowlisted) {
86791
+ // Reject if ANY record is private — we can't trust DNS
86792
+ // round-robin to "win the lottery" and only return public IPs.
86793
+ for (const r of records)
86794
+ assertIpPublic(r.address);
86795
+ }
86796
+ resolvedIp = records[0].address;
86797
+ }
86798
+ return { resolvedIp, originalHost: host };
86799
+ });
86800
+ }
86801
+ function assertIpPublic(ip) {
86802
+ const v = isIP(ip);
86803
+ if (v === 4 && ipv4Blocked(ip)) {
86804
+ throw new Error(`ssrf_blocked: ${ip}`);
86805
+ }
86806
+ if (v === 6 && ipv6Blocked(ip)) {
86807
+ throw new Error(`ssrf_blocked: ${ip}`);
86808
+ }
86809
+ if (v === 0) {
86810
+ throw new Error(`ssrf_blocked: not a valid IP: ${ip}`);
86811
+ }
86812
+ }
86813
+ function ipv4Blocked(ip) {
86814
+ const parts = ip.split('.').map(Number);
86815
+ if (parts.length !== 4 || parts.some((p) => p < 0 || p > 255))
86816
+ return true;
86817
+ const n = ((parts[0] << 24) | (parts[1] << 16) | (parts[2] << 8) | parts[3]) >>> 0;
86818
+ return BLOCKED_V4.some(([net, bits]) => {
86819
+ const mask = bits === 0 ? 0 : ((-1) << (32 - bits)) >>> 0;
86820
+ return (n & mask) === (net & mask);
86821
+ });
86822
+ }
86823
+ /**
86824
+ * Hardcoded IPv6 CIDR blocks rejected outright (not delegated to v4).
86825
+ * Each tuple is [network address as 128-bit bigint, prefix length].
86826
+ *
86827
+ * Note: `BigInt('0x...')` is used (instead of `0x...n` literals) so the
86828
+ * code compiles under TypeScript `target: es6` — bigint literals need
86829
+ * es2020. Semantically identical.
86830
+ */
86831
+ const BLOCKED_V6 = [
86832
+ [BigInt('0xfe800000000000000000000000000000'), 10],
86833
+ [BigInt('0xfc000000000000000000000000000000'), 7],
86834
+ [BigInt('0xff000000000000000000000000000000'), 8],
86835
+ [BigInt('0x01000000000000000000000000000000'), 64],
86836
+ [BigInt('0x20010db8000000000000000000000000'), 32],
86837
+ [BigInt('0x20010000000000000000000000000000'), 32], // 2001::/32 Teredo
86838
+ ];
86839
+ // Top-96-bit prefixes that identify "an IPv4 address carried inside IPv6".
86840
+ // When these match, the low 32 bits are the actual IPv4 and we delegate
86841
+ // the decision to `ipv4Blocked` — so e.g. NAT64 around a public IPv4
86842
+ // passes, NAT64 around a private IPv4 is rejected.
86843
+ const HIGH96_MASK = BigInt('0xffffffffffffffffffffffff00000000');
86844
+ const PREFIX_V4_MAPPED = BigInt('0x00000000000000000000ffff00000000'); // ::ffff:0:0/96
86845
+ const PREFIX_V4_COMPAT = BigInt(0); // ::/96
86846
+ const PREFIX_NAT64 = BigInt('0x0064ff9b000000000000000000000000'); // 64:ff9b::/96
86847
+ const LOW32_MASK = BigInt('0xffffffff');
86848
+ function ipv6Blocked(ip) {
86849
+ let n;
86850
+ try {
86851
+ n = parseIPv6(ip);
86852
+ }
86853
+ catch (_a) {
86854
+ // Unparseable — fail-closed: treat as internal / suspicious.
86855
+ return true;
86856
+ }
86857
+ // If the address carries an IPv4 in its low 32 bits, check that IPv4.
86858
+ const high96 = n & HIGH96_MASK;
86859
+ if (high96 === PREFIX_V4_MAPPED ||
86860
+ high96 === PREFIX_NAT64 ||
86861
+ high96 === PREFIX_V4_COMPAT) {
86862
+ const v4 = Number(n & LOW32_MASK);
86863
+ const v4Str = [
86864
+ (v4 >>> 24) & 0xff,
86865
+ (v4 >>> 16) & 0xff,
86866
+ (v4 >>> 8) & 0xff,
86867
+ v4 & 0xff,
86868
+ ].join('.');
86869
+ return ipv4Blocked(v4Str);
86870
+ }
86871
+ // Standalone blocked CIDRs via numeric comparison — handles all
86872
+ // equivalent string representations (compressed, expanded, mixed).
86873
+ return BLOCKED_V6.some(([net, bits]) => {
86874
+ const shift = BigInt(128 - bits);
86875
+ const allOnes = (BigInt(1) << BigInt(128)) - BigInt(1);
86876
+ const mask = (allOnes >> shift) << shift;
86877
+ return (n & mask) === (net & mask);
86878
+ });
86879
+ }
86880
+ /**
86881
+ * Parse any representation of an IPv6 address into a 128-bit bigint.
86882
+ * Handles:
86883
+ * - `::` compression (`::1`, `fe80::1`, `::`)
86884
+ * - fully expanded form (`0:0:0:0:0:0:0:1`)
86885
+ * - IPv4-in-IPv6 mixed notation (`::ffff:127.0.0.1`, `64:ff9b::10.0.0.1`)
86886
+ * - Zone ID suffix (`fe80::1%eth0` — the suffix is ignored)
86887
+ * Throws on any malformed input; the caller treats a throw as
86888
+ * fail-closed.
86889
+ */
86890
+ function parseIPv6(ip) {
86891
+ let addr = ip.split('%')[0].toLowerCase(); // strip Zone ID
86892
+ // Convert IPv4 suffix (dotted decimal) to two 16-bit hex groups so the
86893
+ // rest of the parser only sees hex groups.
86894
+ const v4Suffix = addr.match(/:(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
86895
+ if (v4Suffix) {
86896
+ const parts = v4Suffix[1].split('.').map(Number);
86897
+ if (parts.length !== 4 || parts.some((p) => !Number.isFinite(p) || p < 0 || p > 255)) {
86898
+ throw new Error('bad v4 suffix');
86899
+ }
86900
+ const hex1 = ((parts[0] << 8) | parts[1]).toString(16);
86901
+ const hex2 = ((parts[2] << 8) | parts[3]).toString(16);
86902
+ addr = addr.slice(0, addr.length - v4Suffix[1].length) + `${hex1}:${hex2}`;
86903
+ }
86904
+ // Expand `::`.
86905
+ const halves = addr.split('::');
86906
+ if (halves.length > 2)
86907
+ throw new Error('multiple "::"');
86908
+ let groups;
86909
+ if (halves.length === 2) {
86910
+ const left = halves[0] ? halves[0].split(':') : [];
86911
+ const right = halves[1] ? halves[1].split(':') : [];
86912
+ const fill = 8 - left.length - right.length;
86913
+ if (fill < 0)
86914
+ throw new Error('too many groups');
86915
+ groups = [...left, ...Array(fill).fill('0'), ...right];
86916
+ }
86917
+ else {
86918
+ groups = addr.split(':');
86919
+ }
86920
+ if (groups.length !== 8)
86921
+ throw new Error('wrong group count');
86922
+ let n = BigInt(0);
86923
+ for (const g of groups) {
86924
+ if (!/^[0-9a-f]{0,4}$/.test(g))
86925
+ throw new Error('bad group');
86926
+ n = (n << BigInt(16)) | BigInt(`0x${g || '0'}`);
86927
+ }
86928
+ return n;
86929
+ }
86930
+
86931
+ /**
86932
+ * POSIX directory prefixes that can never be a legitimate media source.
86933
+ * Skipped on Windows (no equivalent single-prefix set exists there; a
86934
+ * downstream allowlist is the right tool for Windows deployments).
86935
+ */
86936
+ const POSIX_BLOCKED_PREFIXES = ['/etc/', '/proc/', '/sys/', '/dev/'];
86937
+ /** Max bytes accepted from a URL-based media source. Protects against
86938
+ * memory DoS when a malicious URL tries to return gigabytes. */
86939
+ const URL_MAX_BYTES = 50 * 1024 * 1024;
86940
+ class MediaUploader {
86941
+ constructor(client, config) {
86942
+ this.client = client;
86943
+ this.config = config;
86944
+ }
86945
+ upload(input) {
86946
+ var _a, _b, _c;
86947
+ return __awaiter(this, void 0, void 0, function* () {
86948
+ const buffer = yield this.toBuffer(input.source);
86949
+ if (input.kind === 'image') {
86950
+ return this.uploadImage(buffer);
86951
+ }
86952
+ if (input.kind === 'audio') {
86953
+ const duration = this.resolveDuration(input, buffer);
86954
+ return this.uploadFile(buffer, 'opus', (_a = input.fileName) !== null && _a !== void 0 ? _a : 'voice.opus', duration);
86955
+ }
86956
+ if (input.kind === 'video') {
86957
+ const duration = this.resolveDuration(input, buffer);
86958
+ return this.uploadFile(buffer, 'mp4', (_b = input.fileName) !== null && _b !== void 0 ? _b : 'video.mp4', duration);
86959
+ }
86960
+ // generic file — no duration required
86961
+ return this.uploadFile(buffer, 'stream', (_c = input.fileName) !== null && _c !== void 0 ? _c : 'upload.bin');
86962
+ });
86963
+ }
86964
+ /**
86965
+ * Materialize `source` into a Buffer. A string is treated as:
86966
+ * • `http://` / `https://` URL — fetch with SSRF guard
86967
+ * • anything else — local filesystem path, read directly
86968
+ */
86969
+ toBuffer(source) {
86970
+ var _a, _b;
86971
+ return __awaiter(this, void 0, void 0, function* () {
86972
+ if (Buffer.isBuffer(source))
86973
+ return source;
86974
+ if (!/^https?:\/\//i.test(source)) {
86975
+ const allowedDirs = (_a = this.config) === null || _a === void 0 ? void 0 : _a.allowedFileDirs;
86976
+ const resolved = path.resolve(source);
86977
+ // Pre-I/O POSIX blocklist check: catches `/etc/passwd` etc.
86978
+ // before touching the filesystem. Path-traversal variants like
86979
+ // `/tmp/../etc/passwd` are collapsed by `path.resolve`.
86980
+ this.assertNotOnPosixBlocklist(resolved);
86981
+ try {
86982
+ // Follow symlinks so that:
86983
+ // • a symlink inside an allowed dir pointing outside
86984
+ // is caught (allowlist containment is on real target);
86985
+ // • macOS `/etc` → `/private/etc` alias is also blocked.
86986
+ const realPath = yield fs.promises.realpath(resolved);
86987
+ this.assertNotOnPosixBlocklist(realPath);
86988
+ if (allowedDirs && allowedDirs.length > 0) {
86989
+ // Realpath the allowed dirs too — otherwise macOS
86990
+ // `/var` → `/private/var` alias would reject all
86991
+ // tmpdir-based paths.
86992
+ const canonicalDirs = yield Promise.all(allowedDirs.map((d) => __awaiter(this, void 0, void 0, function* () {
86993
+ const r = path.resolve(d);
86994
+ try {
86995
+ return yield fs.promises.realpath(r);
86996
+ }
86997
+ catch (_c) {
86998
+ return r;
86999
+ }
87000
+ })));
87001
+ const inAllowed = canonicalDirs.some((d) => realPath === d || realPath.startsWith(d + path.sep));
87002
+ if (!inAllowed) {
87003
+ throw new LarkChannelError('upload_failed', `file path is outside allowed directories: ${realPath}`);
87004
+ }
87005
+ }
87006
+ return yield fs.promises.readFile(realPath);
87007
+ }
87008
+ catch (e) {
87009
+ if (e instanceof LarkChannelError)
87010
+ throw e;
87011
+ throw new LarkChannelError('upload_failed', `source is neither an http(s) URL nor a readable local file: ${source}`, { cause: e });
87012
+ }
87013
+ }
87014
+ const ssrf = (_b = this.config) === null || _b === void 0 ? void 0 : _b.ssrfGuard;
87015
+ const guardEnabled = ssrf !== false;
87016
+ let resolvedIp;
87017
+ if (guardEnabled) {
87018
+ const ssrfOpts = typeof ssrf === 'object' && ssrf ? { allowlist: ssrf.allowlist } : {};
87019
+ try {
87020
+ ({ resolvedIp } = yield assertPublicUrl(source, ssrfOpts));
87021
+ }
87022
+ catch (e) {
87023
+ throw new LarkChannelError('ssrf_blocked', `URL blocked: ${String(e)}`, {
87024
+ cause: e,
87025
+ });
87026
+ }
87027
+ }
87028
+ try {
87029
+ // Pin the TCP connect target to the IP we just validated so the
87030
+ // attacker can't swap in a private IP between the DNS check and
87031
+ // the fetch (DNS rebinding / TOCTOU). We inject a custom `lookup`
87032
+ // into a per-request agent; the URL itself stays intact so TLS
87033
+ // SNI and certificate verification continue to use the original
87034
+ // hostname.
87035
+ const requestOpts = {
87036
+ url: source,
87037
+ method: 'GET',
87038
+ responseType: 'arraybuffer',
87039
+ timeout: 15000,
87040
+ maxContentLength: URL_MAX_BYTES,
87041
+ maxBodyLength: URL_MAX_BYTES,
87042
+ };
87043
+ if (resolvedIp) {
87044
+ const agent = makePinnedAgent(source, resolvedIp);
87045
+ requestOpts.httpAgent = agent;
87046
+ requestOpts.httpsAgent = agent;
87047
+ }
87048
+ const res = yield this.client.httpInstance.request(requestOpts);
87049
+ return Buffer.from(res);
87050
+ }
87051
+ catch (e) {
87052
+ throw new LarkChannelError('upload_failed', `fetch source URL failed`, {
87053
+ cause: e,
87054
+ });
87055
+ }
87056
+ });
87057
+ }
87058
+ /**
87059
+ * Reject paths pointing at POSIX system directories (`/etc`, `/proc`,
87060
+ * `/sys`, `/dev`). Called twice by the caller — once on the resolved
87061
+ * path pre-I/O, again on the realpath — so both direct hits
87062
+ * (`/etc/passwd`) and macOS alias hits (`/etc` → `/private/etc`) are
87063
+ * caught.
87064
+ */
87065
+ assertNotOnPosixBlocklist(p) {
87066
+ if (process.platform === 'win32')
87067
+ return;
87068
+ if (POSIX_BLOCKED_PREFIXES.some((pre) => p === pre.slice(0, -1) || p.startsWith(pre))) {
87069
+ throw new LarkChannelError('upload_failed', `file path is not allowed: ${p}`);
87070
+ }
87071
+ }
87072
+ resolveDuration(input, buffer) {
87073
+ if (input.duration != null && input.duration > 0)
87074
+ return input.duration;
87075
+ const parsed = input.kind === 'audio'
87076
+ ? parseOpusDuration(buffer)
87077
+ : input.kind === 'video'
87078
+ ? parseMp4Duration(buffer)
87079
+ : undefined;
87080
+ if (parsed != null)
87081
+ return parsed;
87082
+ throw new LarkChannelError('upload_failed', `duration could not be determined for ${input.kind}; pass it explicitly`);
87083
+ }
87084
+ uploadImage(buffer) {
87085
+ var _a, _b;
87086
+ return __awaiter(this, void 0, void 0, function* () {
87087
+ try {
87088
+ const r = yield this.client.im.v1.image.create({
87089
+ data: { image_type: 'message', image: buffer },
87090
+ });
87091
+ // The code-gen client already strips the outer envelope and returns
87092
+ // `res?.data` directly, so `image_key` sits at the top level. Keep
87093
+ // the nested `.data.image_key` path as a defensive fallback in case
87094
+ // a caller plugs in a different HttpInstance that preserves the
87095
+ // envelope.
87096
+ const key = (_a = r === null || r === void 0 ? void 0 : r.image_key) !== null && _a !== void 0 ? _a : (_b = r === null || r === void 0 ? void 0 : r.data) === null || _b === void 0 ? void 0 : _b.image_key;
87097
+ if (!key)
87098
+ throw new Error('image_key missing in upload response');
87099
+ return { kind: 'image', fileKey: key };
87100
+ }
87101
+ catch (e) {
87102
+ throw new LarkChannelError('upload_failed', `image upload failed`, {
87103
+ cause: e,
87104
+ });
87105
+ }
87106
+ });
87107
+ }
87108
+ uploadFile(buffer, fileType, fileName, durationMs) {
87109
+ var _a, _b;
87110
+ return __awaiter(this, void 0, void 0, function* () {
87111
+ try {
87112
+ const data = {
87113
+ file_type: fileType,
87114
+ file_name: fileName,
87115
+ file: buffer,
87116
+ };
87117
+ if (durationMs != null)
87118
+ data.duration = durationMs;
87119
+ const r = yield this.client.im.v1.file.create({
87120
+ data: data,
87121
+ });
87122
+ const key = (_a = r === null || r === void 0 ? void 0 : r.file_key) !== null && _a !== void 0 ? _a : (_b = r === null || r === void 0 ? void 0 : r.data) === null || _b === void 0 ? void 0 : _b.file_key;
87123
+ if (!key)
87124
+ throw new Error('file_key missing in upload response');
87125
+ const kind = fileType === 'opus' ? 'audio' : fileType === 'mp4' ? 'video' : 'file';
87126
+ return { kind, fileKey: key, durationMs };
87127
+ }
87128
+ catch (e) {
87129
+ if (e instanceof LarkChannelError)
87130
+ throw e;
87131
+ throw new LarkChannelError('upload_failed', `file upload failed`, {
87132
+ cause: e,
87133
+ });
87134
+ }
87135
+ });
87136
+ }
87137
+ }
87138
+ /**
87139
+ * Build a per-request http(s) Agent whose DNS lookup always returns
87140
+ * `pinnedIp`, regardless of what hostname Node would otherwise resolve.
87141
+ * The URL's original hostname is preserved on the wire, which keeps TLS
87142
+ * SNI and certificate verification working. This closes the window where
87143
+ * a malicious DNS server could return a different (private) IP on the
87144
+ * second resolution triggered by the actual fetch.
87145
+ */
87146
+ function makePinnedAgent(url, pinnedIp) {
87147
+ const AgentClass = url.startsWith('https:') ? https.Agent : http.Agent;
87148
+ const agent = new AgentClass();
87149
+ const family = pinnedIp.includes(':') ? 6 : 4;
87150
+ const lookup = (_hostname, _opts, cb) => {
87151
+ cb(null, pinnedIp, family);
87152
+ };
87153
+ // Node's Agent doesn't accept `lookup` in its constructor options, so we
87154
+ // wrap `createConnection` to fold `lookup` into every outgoing socket.
87155
+ const origCreateConnection = agent.createConnection.bind(agent);
87156
+ agent.createConnection = (opts, cb) => origCreateConnection(Object.assign(Object.assign({}, opts), { lookup }), cb);
87157
+ return agent;
87158
+ }
87159
+
87160
+ /**
87161
+ * Build a text prefix that renders as real Feishu mentions when prepended
87162
+ * to a text-type outbound message (the <at …> tag form).
87163
+ *
87164
+ * For post-type messages, the mentions should be injected as `at` elements
87165
+ * at the beginning of the post body — use `composePostMentionElements`
87166
+ * instead.
87167
+ */
87168
+ function composeMentionsTextPrefix(mentions) {
87169
+ var _a;
87170
+ if (!(mentions === null || mentions === void 0 ? void 0 : mentions.length))
87171
+ return '';
87172
+ const parts = [];
87173
+ for (const m of mentions) {
87174
+ if (!m.openId)
87175
+ continue;
87176
+ const name = (_a = m.name) !== null && _a !== void 0 ? _a : '';
87177
+ parts.push(`<at user_id="${m.openId}">${name}</at>`);
87178
+ }
87179
+ return parts.length > 0 ? parts.join(' ') + ' ' : '';
87180
+ }
87181
+ /**
87182
+ * Produce `at` elements to prepend to the first paragraph of a post body.
87183
+ */
87184
+ function composePostMentionElements(mentions) {
87185
+ if (!(mentions === null || mentions === void 0 ? void 0 : mentions.length))
87186
+ return [];
87187
+ const out = [];
87188
+ for (const m of mentions) {
87189
+ if (!m.openId)
87190
+ continue;
87191
+ out.push({ tag: 'at', user_id: m.openId, user_name: m.name });
87192
+ }
87193
+ return out;
87194
+ }
87195
+
87196
+ /**
87197
+ * Convert a Markdown string to Feishu post JSON.
87198
+ *
87199
+ * Coverage (the subset Agent frameworks realistically produce):
87200
+ * - Headings (rendered as bold text; Feishu post has no heading element)
87201
+ * - Paragraphs with inline **bold**, *italic*, `code`, [text](url)
87202
+ * - Fenced code blocks (```lang\n...\n```)
87203
+ * - Hard rules (---)
87204
+ * - Unordered / ordered list items (rendered as plain text with bullets)
87205
+ * - Blockquotes (rendered as "> " prefix preserved)
87206
+ *
87207
+ * Returns a post body keyed by `zh_cn` (Feishu's default locale).
87208
+ */
87209
+ function markdownToPost(md, opts) {
87210
+ var _a, _b;
87211
+ const lines = md.replace(/\r\n/g, '\n').split('\n');
87212
+ const paragraphs = [];
87213
+ let fenceLang = null;
87214
+ let fenceBuf = [];
87215
+ const flushFence = () => {
87216
+ if (fenceLang !== null) {
87217
+ paragraphs.push([
87218
+ {
87219
+ tag: 'text',
87220
+ text: fenceBuf.join('\n'),
87221
+ un_escape: true,
87222
+ },
87223
+ ]);
87224
+ fenceBuf = [];
87225
+ fenceLang = null;
87226
+ }
87227
+ };
87228
+ for (const line of lines) {
87229
+ const fenceMatch = line.match(/^```(\w*)\s*$/);
87230
+ if (fenceMatch) {
87231
+ if (fenceLang !== null) {
87232
+ flushFence();
87233
+ }
87234
+ else {
87235
+ fenceLang = fenceMatch[1] || '';
87236
+ }
87237
+ continue;
87238
+ }
87239
+ if (fenceLang !== null) {
87240
+ fenceBuf.push(line);
87241
+ continue;
87242
+ }
87243
+ if (/^\s*$/.test(line)) {
87244
+ paragraphs.push([{ tag: 'text', text: '' }]);
87245
+ continue;
87246
+ }
87247
+ if (/^(-{3,}|_{3,}|\*{3,})\s*$/.test(line)) {
87248
+ paragraphs.push([{ tag: 'text', text: '———' }]);
87249
+ continue;
87250
+ }
87251
+ paragraphs.push(parseInline(line));
87252
+ }
87253
+ flushFence();
87254
+ // Prepend explicit mentions as `at` elements at the very top.
87255
+ if ((_a = opts === null || opts === void 0 ? void 0 : opts.mentions) === null || _a === void 0 ? void 0 : _a.length) {
87256
+ const atElements = composePostMentionElements(opts.mentions).map((el) => el);
87257
+ if (atElements.length > 0) {
87258
+ // Insert as a new first paragraph with at elements + a space
87259
+ const first = [];
87260
+ for (const el of atElements) {
87261
+ first.push(el);
87262
+ first.push({ tag: 'text', text: ' ' });
87263
+ }
87264
+ paragraphs.unshift(first);
87265
+ }
87266
+ }
87267
+ return {
87268
+ zh_cn: {
87269
+ title: (_b = opts === null || opts === void 0 ? void 0 : opts.title) !== null && _b !== void 0 ? _b : '',
87270
+ content: paragraphs,
87271
+ },
87272
+ };
87273
+ }
87274
+ /**
87275
+ * Parse a single line of inline markdown into a list of post elements.
87276
+ *
87277
+ * Handles (in order):
87278
+ * ```inline code```, [text](url), **bold**, *italic*, __bold__, _italic_
87279
+ *
87280
+ * Headings (leading `#` sequence) render as bold text. List bullets and
87281
+ * blockquote markers pass through as plain text prefix.
87282
+ */
87283
+ function parseInline(line) {
87284
+ // Normalize headings to bold
87285
+ const headingMatch = line.match(/^(#{1,6})\s+(.*)$/);
87286
+ if (headingMatch) {
87287
+ const boldText = headingMatch[2];
87288
+ return [{ tag: 'text', text: boldText, style: ['bold'] }];
87289
+ }
87290
+ const out = [];
87291
+ // Token regex: inline code, link, bold, italic
87292
+ const pattern = /(`[^`\n]+`)|(\[([^\]]+)\]\(([^)]+)\))|(\*\*[^*\n]+\*\*)|(__[^_\n]+__)|(\*[^*\n]+\*)|(_[^_\n]+_)/g;
87293
+ let last = 0;
87294
+ let m;
87295
+ while ((m = pattern.exec(line))) {
87296
+ if (m.index > last) {
87297
+ out.push({ tag: 'text', text: line.slice(last, m.index) });
87298
+ }
87299
+ if (m[1]) {
87300
+ // inline code
87301
+ out.push({ tag: 'text', text: m[1].slice(1, -1), style: ['code'] });
87302
+ }
87303
+ else if (m[2]) {
87304
+ // link
87305
+ out.push({ tag: 'a', text: m[3], href: m[4] });
87306
+ }
87307
+ else if (m[5]) {
87308
+ out.push({ tag: 'text', text: m[5].slice(2, -2), style: ['bold'] });
87309
+ }
87310
+ else if (m[6]) {
87311
+ out.push({ tag: 'text', text: m[6].slice(2, -2), style: ['bold'] });
87312
+ }
87313
+ else if (m[7]) {
87314
+ out.push({ tag: 'text', text: m[7].slice(1, -1), style: ['italic'] });
87315
+ }
87316
+ else if (m[8]) {
87317
+ out.push({ tag: 'text', text: m[8].slice(1, -1), style: ['italic'] });
87318
+ }
87319
+ last = pattern.lastIndex;
87320
+ }
87321
+ if (last < line.length) {
87322
+ out.push({ tag: 'text', text: line.slice(last) });
87323
+ }
87324
+ return out.length > 0 ? out : [{ tag: 'text', text: line }];
87325
+ }
87326
+ /**
87327
+ * Extract the plain text body from a post JSON (recover flatted text for
87328
+ * fallback-to-text paths).
87329
+ */
87330
+ function postToPlainText(post) {
87331
+ var _a;
87332
+ const body = post === null || post === void 0 ? void 0 : post.zh_cn;
87333
+ if (!(body === null || body === void 0 ? void 0 : body.content))
87334
+ return '';
87335
+ const lines = [];
87336
+ for (const paragraph of body.content) {
87337
+ if (!Array.isArray(paragraph))
87338
+ continue;
87339
+ const parts = [];
87340
+ for (const el of paragraph) {
87341
+ switch (el.tag) {
87342
+ case 'text':
87343
+ case 'a':
87344
+ parts.push((_a = el.text) !== null && _a !== void 0 ? _a : '');
87345
+ break;
87346
+ case 'at':
87347
+ parts.push(el.user_name ? `@${el.user_name}` : '');
87348
+ break;
87349
+ case 'img':
87350
+ parts.push(el.image_key ? `[image]` : '');
87351
+ break;
87352
+ }
87353
+ }
87354
+ lines.push(parts.join(''));
87355
+ }
87356
+ return lines.join('\n').trim();
87357
+ }
87358
+
87359
+ /**
87360
+ * Split a long markdown string into chunks under `limit` characters.
87361
+ *
87362
+ * Preserves code block integrity — if a chunk boundary would fall inside a
87363
+ * fenced code block, the current chunk closes the fence and the next chunk
87364
+ * reopens it with the same language tag.
87365
+ *
87366
+ * Prefers breaking before headings when possible.
87367
+ */
87368
+ function splitWithCodeFences(text, limit) {
87369
+ if (text.length <= limit)
87370
+ return [text];
87371
+ const lines = text.split('\n');
87372
+ const out = [];
87373
+ let buf = [];
87374
+ let bufLen = 0;
87375
+ let fenceLang = null;
87376
+ const flush = () => {
87377
+ if (buf.length === 0)
87378
+ return;
87379
+ let chunk = buf.join('\n');
87380
+ if (fenceLang !== null)
87381
+ chunk += '\n```';
87382
+ out.push(chunk);
87383
+ buf = [];
87384
+ bufLen = 0;
87385
+ if (fenceLang !== null) {
87386
+ // reopen fence in the next chunk
87387
+ buf.push('```' + fenceLang);
87388
+ bufLen = buf[0].length;
87389
+ }
87390
+ };
87391
+ for (const line of lines) {
87392
+ const m = line.match(/^```(\w*)$/);
87393
+ const lineLen = line.length + (buf.length > 0 ? 1 : 0); // +1 for \n
87394
+ // If this line is a heading and we already have content that's near
87395
+ // the limit, prefer breaking here.
87396
+ const isHeading = /^#{1,6}\s/.test(line);
87397
+ const nearFull = bufLen > limit * 0.75;
87398
+ if (bufLen + lineLen > limit || (isHeading && nearFull && buf.length > 0)) {
87399
+ flush();
87400
+ }
87401
+ buf.push(line);
87402
+ bufLen += lineLen;
87403
+ if (m) {
87404
+ // entering or leaving a fence
87405
+ fenceLang = fenceLang === null ? m[1] || '' : null;
87406
+ }
87407
+ }
87408
+ flush();
87409
+ return out;
87410
+ }
87411
+
87412
+ /**
87413
+ * Infer Feishu's `receive_id_type` from the prefix of a target id.
87414
+ *
87415
+ * oc_* → chat_id
87416
+ * ou_* → open_id
87417
+ * on_* → union_id
87418
+ * contains '@' → email
87419
+ * fallback → user_id
87420
+ */
87421
+ function detectReceiveIdType(to) {
87422
+ if (!to)
87423
+ throw new Error('empty receive_id');
87424
+ if (to.startsWith('oc_'))
87425
+ return 'chat_id';
87426
+ if (to.startsWith('ou_'))
87427
+ return 'open_id';
87428
+ if (to.startsWith('on_'))
87429
+ return 'union_id';
87430
+ if (to.includes('@'))
87431
+ return 'email';
87432
+ return 'user_id';
87433
+ }
87434
+
87435
+ /**
87436
+ * Classify a raw error (typically from axios/fetch or a Feishu API response)
87437
+ * into a LarkChannelError with a stable code.
87438
+ */
87439
+ function classifyError(err, context) {
87440
+ if (err instanceof LarkChannelError)
87441
+ return err;
87442
+ const code = inferCode(err);
87443
+ const message = extractMessage(err);
87444
+ return new LarkChannelError(code, message, { cause: err, context });
87445
+ }
87446
+ function inferCode(err) {
87447
+ var _a, _b, _c, _d, _e, _f, _g, _h;
87448
+ const raw = err;
87449
+ const status = (_b = (_a = raw === null || raw === void 0 ? void 0 : raw.response) === null || _a === void 0 ? void 0 : _a.status) !== null && _b !== void 0 ? _b : raw === null || raw === void 0 ? void 0 : raw.status;
87450
+ const feishuCode = (_g = (_e = (_d = (_c = raw === null || raw === void 0 ? void 0 : raw.response) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.code) !== null && _e !== void 0 ? _e : (_f = raw === null || raw === void 0 ? void 0 : raw.data) === null || _f === void 0 ? void 0 : _f.code) !== null && _g !== void 0 ? _g : raw === null || raw === void 0 ? void 0 : raw.code;
87451
+ const msg = String((_h = raw === null || raw === void 0 ? void 0 : raw.message) !== null && _h !== void 0 ? _h : '').toLowerCase();
87452
+ if (typeof feishuCode === 'number') {
87453
+ if (feishuCode === 230020 || feishuCode === 230017)
87454
+ return 'target_revoked';
87455
+ if (feishuCode === 99991400 || feishuCode === 99991401)
87456
+ return 'permission_denied';
87457
+ if (feishuCode === 230002 || feishuCode === 230001)
87458
+ return 'format_error';
87459
+ }
87460
+ if (status === 429)
87461
+ return 'rate_limited';
87462
+ if (status === 401 || status === 403)
87463
+ return 'permission_denied';
87464
+ if (status === 400)
87465
+ return 'format_error';
87466
+ if (status === 404)
87467
+ return 'target_revoked';
87468
+ if (msg.startsWith('ssrf_blocked'))
87469
+ return 'ssrf_blocked';
87470
+ if (msg.includes('timeout') || (raw === null || raw === void 0 ? void 0 : raw.code) === 'ETIMEDOUT' || (raw === null || raw === void 0 ? void 0 : raw.code) === 'ECONNABORTED') {
87471
+ return 'send_timeout';
87472
+ }
87473
+ return 'unknown';
87474
+ }
87475
+ function extractMessage(err) {
87476
+ var _a, _b, _c, _d;
87477
+ const raw = err;
87478
+ return (((_b = (_a = raw === null || raw === void 0 ? void 0 : raw.response) === null || _a === void 0 ? void 0 : _a.data) === null || _b === void 0 ? void 0 : _b.msg) ||
87479
+ ((_d = (_c = raw === null || raw === void 0 ? void 0 : raw.response) === null || _c === void 0 ? void 0 : _c.data) === null || _d === void 0 ? void 0 : _d.message) ||
87480
+ (raw === null || raw === void 0 ? void 0 : raw.message) ||
87481
+ String(err));
87482
+ }
87483
+ function isRetryable(err) {
87484
+ return err.code === 'rate_limited' || err.code === 'unknown';
87485
+ }
87486
+ function isFormatError(err) {
87487
+ return err.code === 'format_error';
87488
+ }
87489
+ function isReplyTargetGone(err) {
87490
+ return err.code === 'target_revoked';
87491
+ }
87492
+
87493
+ /**
87494
+ * Execute `op` with exponential backoff. Only retries errors classified as
87495
+ * retryable (rate_limited / unknown). Business errors (format / revoked /
87496
+ * permission / timeout) fail fast and bubble up.
87497
+ */
87498
+ function retry(op, opts = {}) {
87499
+ var _a, _b;
87500
+ return __awaiter(this, void 0, void 0, function* () {
87501
+ const max = (_a = opts.maxAttempts) !== null && _a !== void 0 ? _a : 3;
87502
+ const base = (_b = opts.baseDelayMs) !== null && _b !== void 0 ? _b : 500;
87503
+ let lastErr;
87504
+ for (let attempt = 1; attempt <= max; attempt++) {
87505
+ try {
87506
+ return yield op(attempt);
87507
+ }
87508
+ catch (raw) {
87509
+ const err = classifyError(raw, { attempt });
87510
+ lastErr = err;
87511
+ if (attempt >= max || !isRetryable(err)) {
87512
+ throw err;
87513
+ }
87514
+ const delay = base * Math.pow(3, attempt - 1);
87515
+ yield sleep(delay);
87516
+ }
87517
+ }
87518
+ throw lastErr;
87519
+ });
87520
+ }
87521
+ function sleep(ms) {
87522
+ return new Promise((r) => setTimeout(r, ms));
87523
+ }
87524
+
87525
+ const DEFAULT_CHUNK_LIMIT = 3500;
87526
+ class OutboundSender {
87527
+ constructor(client, config, logger) {
87528
+ var _a;
87529
+ this.client = client;
87530
+ this.config = config;
87531
+ this.logger = logger;
87532
+ this.uploader = new MediaUploader(client, config);
87533
+ this.chunkLimit = (_a = config.textChunkLimit) !== null && _a !== void 0 ? _a : DEFAULT_CHUNK_LIMIT;
87534
+ }
87535
+ send(to, input, opts = {}) {
87536
+ return __awaiter(this, void 0, void 0, function* () {
87537
+ const idType = detectReceiveIdType(to);
87538
+ if ('markdown' in input)
87539
+ return this.sendMarkdown(to, idType, input.markdown, opts);
87540
+ if ('text' in input)
87541
+ return this.sendText(to, idType, input.text, opts);
87542
+ if ('post' in input)
87543
+ return this.sendPost(to, idType, input.post, opts);
87544
+ if ('image' in input)
87545
+ return this.sendImage(to, idType, input.image, opts);
87546
+ if ('file' in input)
87547
+ return this.sendFile(to, idType, input.file, opts);
87548
+ if ('audio' in input)
87549
+ return this.sendAudio(to, idType, input.audio, opts);
87550
+ if ('video' in input)
87551
+ return this.sendVideo(to, idType, input.video, opts);
87552
+ if ('card' in input)
87553
+ return this.sendCard(to, idType, input.card, opts);
87554
+ if ('shareChat' in input)
87555
+ return this.sendShareChat(to, idType, input.shareChat.chatId, opts);
87556
+ if ('shareUser' in input)
87557
+ return this.sendShareUser(to, idType, input.shareUser.userId, opts);
87558
+ if ('sticker' in input)
87559
+ return this.sendSticker(to, idType, input.sticker.fileKey, opts);
87560
+ throw new LarkChannelError('format_error', 'unrecognized SendInput shape');
87561
+ });
87562
+ }
87563
+ stream(to, input, opts = {}) {
87564
+ return __awaiter(this, void 0, void 0, function* () {
87565
+ const idType = detectReceiveIdType(to);
87566
+ if ('markdown' in input) {
87567
+ const controller = new MarkdownStreamController(this, to, idType, opts);
87568
+ return controller.run(input.markdown);
87569
+ }
87570
+ if ('card' in input) {
87571
+ const controller = new CardStreamController(this, to, idType, opts, input.card.initial);
87572
+ return controller.run(input.card.producer);
87573
+ }
87574
+ throw new LarkChannelError('format_error', 'unrecognized StreamInput shape');
87575
+ });
87576
+ }
87577
+ // ─── text / markdown / post ───────────────────────────
87578
+ sendMarkdown(to, idType, md, opts) {
87579
+ return __awaiter(this, void 0, void 0, function* () {
87580
+ const chunks = splitWithCodeFences(md, this.chunkLimit);
87581
+ const ids = [];
87582
+ for (let i = 0; i < chunks.length; i++) {
87583
+ const post = this.convertMarkdown(chunks[i], i === 0 ? opts.mentions : undefined);
87584
+ const id = yield this.sendOneWithFallback({
87585
+ to, idType,
87586
+ msgType: 'post', content: post,
87587
+ replyTo: i === 0 ? opts.replyTo : undefined,
87588
+ replyInThread: opts.replyInThread,
87589
+ });
87590
+ ids.push(id);
87591
+ }
87592
+ return this.makeResult(ids);
87593
+ });
87594
+ }
87595
+ sendText(to, idType, text, opts) {
87596
+ var _a;
87597
+ return __awaiter(this, void 0, void 0, function* () {
87598
+ const prefix = composeMentionsTextPrefix((_a = opts.mentions) !== null && _a !== void 0 ? _a : []);
87599
+ const body = prefix + text;
87600
+ const chunks = splitPlain(body, this.chunkLimit);
87601
+ const ids = [];
87602
+ for (let i = 0; i < chunks.length; i++) {
87603
+ const id = yield this.sendOneWithFallback({
87604
+ to, idType,
87605
+ msgType: 'text',
87606
+ content: { text: chunks[i] },
87607
+ replyTo: i === 0 ? opts.replyTo : undefined,
87608
+ replyInThread: opts.replyInThread,
87609
+ });
87610
+ ids.push(id);
87611
+ }
87612
+ return this.makeResult(ids);
87613
+ });
87614
+ }
87615
+ sendPost(to, idType, post, opts) {
87616
+ return __awaiter(this, void 0, void 0, function* () {
87617
+ const id = yield this.sendOneWithFallback({
87618
+ to, idType,
87619
+ msgType: 'post',
87620
+ content: post,
87621
+ replyTo: opts.replyTo,
87622
+ replyInThread: opts.replyInThread,
87623
+ });
87624
+ return { messageId: id };
87625
+ });
87626
+ }
87627
+ convertMarkdown(md, mentions) {
87628
+ const conv = this.config.markdownConverter;
87629
+ if (typeof conv === 'function')
87630
+ return conv(md);
87631
+ return markdownToPost(md, { mentions });
87632
+ }
87633
+ // ─── media ────────────────────────────────────────────
87634
+ sendImage(to, idType, input, opts) {
87635
+ return __awaiter(this, void 0, void 0, function* () {
87636
+ const up = yield this.uploader.upload({ kind: 'image', source: input.source });
87637
+ const id = yield this.sendOneWithFallback({
87638
+ to, idType,
87639
+ msgType: 'image',
87640
+ content: { image_key: up.fileKey },
87641
+ replyTo: opts.replyTo,
87642
+ replyInThread: opts.replyInThread,
87643
+ });
87644
+ return { messageId: id };
87645
+ });
87646
+ }
87647
+ sendFile(to, idType, input, opts) {
87648
+ return __awaiter(this, void 0, void 0, function* () {
87649
+ const up = yield this.uploader.upload({
87650
+ kind: 'file', source: input.source, fileName: input.fileName,
87651
+ });
87652
+ const id = yield this.sendOneWithFallback({
87653
+ to, idType,
87654
+ msgType: 'file',
87655
+ content: { file_key: up.fileKey },
87656
+ replyTo: opts.replyTo,
87657
+ replyInThread: opts.replyInThread,
87658
+ });
87659
+ return { messageId: id };
87660
+ });
87661
+ }
87662
+ sendAudio(to, idType, input, opts) {
87663
+ return __awaiter(this, void 0, void 0, function* () {
87664
+ const up = yield this.uploader.upload({
87665
+ kind: 'audio', source: input.source, duration: input.duration,
87666
+ });
87667
+ const id = yield this.sendOneWithFallback({
87668
+ to, idType,
87669
+ msgType: 'audio',
87670
+ content: { file_key: up.fileKey },
87671
+ replyTo: opts.replyTo,
87672
+ replyInThread: opts.replyInThread,
87673
+ });
87674
+ return { messageId: id };
87675
+ });
87676
+ }
87677
+ sendVideo(to, idType, input, opts) {
87678
+ return __awaiter(this, void 0, void 0, function* () {
87679
+ const up = yield this.uploader.upload({
87680
+ kind: 'video', source: input.source, duration: input.duration,
87681
+ });
87682
+ const content = { file_key: up.fileKey };
87683
+ if (input.coverImageKey)
87684
+ content.image_key = input.coverImageKey;
87685
+ const id = yield this.sendOneWithFallback({
87686
+ to, idType,
87687
+ msgType: 'media',
87688
+ content,
87689
+ replyTo: opts.replyTo,
87690
+ replyInThread: opts.replyInThread,
87691
+ });
87692
+ return { messageId: id };
87693
+ });
87694
+ }
87695
+ // ─── card ─────────────────────────────────────────────
87696
+ sendCard(to, idType, card, opts) {
87697
+ return __awaiter(this, void 0, void 0, function* () {
87698
+ const id = yield this.sendOneWithFallback({
87699
+ to, idType,
87700
+ msgType: 'interactive',
87701
+ content: card,
87702
+ replyTo: opts.replyTo,
87703
+ replyInThread: opts.replyInThread,
87704
+ });
87705
+ return { messageId: id };
87706
+ });
87707
+ }
87708
+ // ─── share / sticker ──────────────────────────────────
87709
+ sendShareChat(to, idType, chatId, opts) {
87710
+ return __awaiter(this, void 0, void 0, function* () {
87711
+ const id = yield this.sendOneWithFallback({
87712
+ to, idType,
87713
+ msgType: 'share_chat',
87714
+ content: { chat_id: chatId },
87715
+ replyTo: opts.replyTo,
87716
+ replyInThread: opts.replyInThread,
87717
+ });
87718
+ return { messageId: id };
87719
+ });
87720
+ }
87721
+ sendShareUser(to, idType, userId, opts) {
87722
+ return __awaiter(this, void 0, void 0, function* () {
87723
+ const id = yield this.sendOneWithFallback({
87724
+ to, idType,
87725
+ msgType: 'share_user',
87726
+ content: { user_id: userId },
87727
+ replyTo: opts.replyTo,
87728
+ replyInThread: opts.replyInThread,
87729
+ });
87730
+ return { messageId: id };
87731
+ });
87732
+ }
87733
+ sendSticker(to, idType, fileKey, opts) {
87734
+ return __awaiter(this, void 0, void 0, function* () {
87735
+ const id = yield this.sendOneWithFallback({
87736
+ to, idType,
87737
+ msgType: 'sticker',
87738
+ content: { file_key: fileKey },
87739
+ replyTo: opts.replyTo,
87740
+ replyInThread: opts.replyInThread,
87741
+ });
87742
+ return { messageId: id };
87743
+ });
87744
+ }
87745
+ // ─── low-level raw send with fallback & retry ─────────
87746
+ /**
87747
+ * Send once with fallback for format errors (post → text) and for
87748
+ * vanished reply targets (reply → create).
87749
+ */
87750
+ sendOneWithFallback(args) {
87751
+ return __awaiter(this, void 0, void 0, function* () {
87752
+ try {
87753
+ return yield this.rawSendWithRetry(args);
87754
+ }
87755
+ catch (e) {
87756
+ const err = classifyError(e, { to: args.to });
87757
+ if (isReplyTargetGone(err) && args.replyTo) {
87758
+ return this.rawSendWithRetry(Object.assign(Object.assign({}, args), { replyTo: undefined }));
87759
+ }
87760
+ if (isFormatError(err) && args.msgType === 'post') {
87761
+ const plain = postToPlainText(args.content);
87762
+ return this.rawSendWithRetry(Object.assign(Object.assign({}, args), { msgType: 'text', content: { text: plain || '[message]' } }));
87763
+ }
87764
+ throw err;
87765
+ }
87766
+ });
87767
+ }
87768
+ rawSendWithRetry(args) {
87769
+ return __awaiter(this, void 0, void 0, function* () {
87770
+ return retry(() => this.rawSend(args), this.config.retry);
87771
+ });
87772
+ }
87773
+ rawSend(args) {
87774
+ var _a, _b;
87775
+ return __awaiter(this, void 0, void 0, function* () {
87776
+ const payload = {
87777
+ receive_id: args.to,
87778
+ msg_type: args.msgType,
87779
+ content: JSON.stringify(args.content),
87780
+ };
87781
+ if (args.replyTo) {
87782
+ const r = yield this.client.im.v1.message.reply({
87783
+ path: { message_id: args.replyTo },
87784
+ data: {
87785
+ content: payload.content,
87786
+ msg_type: args.msgType,
87787
+ reply_in_thread: args.replyInThread,
87788
+ },
87789
+ });
87790
+ const id = (_a = r.data) === null || _a === void 0 ? void 0 : _a.message_id;
87791
+ if (!id)
87792
+ throw new LarkChannelError('unknown', 'message_id missing from reply response');
87793
+ return id;
87794
+ }
87795
+ const r = yield this.client.im.v1.message.create({
87796
+ params: { receive_id_type: args.idType },
87797
+ data: payload,
87798
+ });
87799
+ const id = (_b = r.data) === null || _b === void 0 ? void 0 : _b.message_id;
87800
+ if (!id)
87801
+ throw new LarkChannelError('unknown', 'message_id missing from create response');
87802
+ return id;
87803
+ });
87804
+ }
87805
+ // ─── helpers used by streaming ────────────────────────
87806
+ /**
87807
+ * Full card replace via im.v1.message.patch — used by CardStreamController
87808
+ * and the public `channel.updateCard()` low-level API. Not suitable for
87809
+ * text streaming (use native cardkit streaming below for that).
87810
+ */
87811
+ patchCard(messageId, card) {
87812
+ return __awaiter(this, void 0, void 0, function* () {
87813
+ yield this.client.im.v1.message.patch({
87814
+ path: { message_id: messageId },
87815
+ data: { content: JSON.stringify(card) },
87816
+ });
87817
+ });
87818
+ }
87819
+ /**
87820
+ * Create a card instance via cardkit.v1.card.create. Used to get a
87821
+ * `card_id` that can be referenced from messages AND updated with
87822
+ * native streaming APIs (cardElement.content for typewriter effect).
87823
+ */
87824
+ createCardInstance(spec) {
87825
+ var _a;
87826
+ return __awaiter(this, void 0, void 0, function* () {
87827
+ const r = yield this.client.cardkit.v1.card.create({
87828
+ data: { type: 'card_json', data: JSON.stringify(spec) },
87829
+ });
87830
+ const cardId = (_a = r.data) === null || _a === void 0 ? void 0 : _a.card_id;
87831
+ if (!cardId) {
87832
+ throw new LarkChannelError('unknown', 'cardkit.card.create returned no card_id');
87833
+ }
87834
+ return cardId;
87835
+ });
87836
+ }
87837
+ /**
87838
+ * Send an interactive message that references a pre-created card instance
87839
+ * by card_id. Returns the resulting message_id.
87840
+ */
87841
+ sendCardByReference(to, idType, cardId, opts) {
87842
+ return __awaiter(this, void 0, void 0, function* () {
87843
+ return this.rawSendWithRetry({
87844
+ to, idType,
87845
+ msgType: 'interactive',
87846
+ content: { type: 'card', data: { card_id: cardId } },
87847
+ replyTo: opts.replyTo,
87848
+ replyInThread: opts.replyInThread,
87849
+ });
87850
+ });
87851
+ }
87852
+ /**
87853
+ * Stream update: replace a card element's content and let Feishu render
87854
+ * the incremental diff as a typewriter animation. `sequence` must be
87855
+ * monotonically increasing per card (duplicates/out-of-order → rejected
87856
+ * server-side). `uuid` is required for idempotency — we compose it from
87857
+ * cardId + sequence so two concurrent calls with the same seq would
87858
+ * dedupe rather than both take effect.
87859
+ */
87860
+ updateCardElementContent(cardId, elementId, content, sequence) {
87861
+ return __awaiter(this, void 0, void 0, function* () {
87862
+ yield this.client.cardkit.v1.cardElement.content({
87863
+ path: { card_id: cardId, element_id: elementId },
87864
+ data: {
87865
+ content,
87866
+ sequence,
87867
+ uuid: `c_${cardId}_${sequence}`,
87868
+ },
87869
+ });
87870
+ });
87871
+ }
87872
+ /**
87873
+ * Switch a streaming card to finalized state (streaming_mode: false).
87874
+ * Feishu auto-closes after 10min regardless, but callers should close
87875
+ * explicitly when producer completes.
87876
+ *
87877
+ * `summary` is optional; when provided, also updates the card's preview
87878
+ * text (the one shown in message lists / chat previews). Without it, the
87879
+ * preview stays at whatever was set during streaming (typically the
87880
+ * default "[Generating...]"), which looks stuck.
87881
+ */
87882
+ finishStreamingCard(cardId, sequence, summary) {
87883
+ return __awaiter(this, void 0, void 0, function* () {
87884
+ const config = { streaming_mode: false };
87885
+ if (summary !== undefined) {
87886
+ config.summary = { content: summary };
87887
+ }
87888
+ yield this.client.cardkit.v1.card.settings({
87889
+ path: { card_id: cardId },
87890
+ data: {
87891
+ settings: JSON.stringify({ config }),
87892
+ sequence,
87893
+ uuid: `s_${cardId}_${sequence}`,
87894
+ },
87895
+ });
87896
+ });
87897
+ }
87898
+ makeResult(ids) {
87899
+ return {
87900
+ messageId: ids[0],
87901
+ chunkIds: ids.length > 1 ? ids : undefined,
87902
+ };
87903
+ }
87904
+ }
87905
+ function splitPlain(text, limit) {
87906
+ if (text.length <= limit)
87907
+ return [text];
87908
+ const out = [];
87909
+ for (let i = 0; i < text.length; i += limit) {
87910
+ out.push(text.slice(i, i + limit));
87911
+ }
87912
+ return out;
87913
+ }
87914
+
87915
+ /**
87916
+ * Per-scope pipeline that does two things at once:
87917
+ * - Debounce-based batch aggregation (for IM messages)
87918
+ * - Strict serialization of all work (both batched flushes and direct
87919
+ * `run()` tasks) via a promise chain
87920
+ *
87921
+ * Supports two entry points:
87922
+ * - push(msg, handler): enqueue for batched dispatch
87923
+ * - run(task): run a one-shot async task serialized after any
87924
+ * pending batch and previous tasks
87925
+ */
87926
+ class ChatPipeline {
87927
+ constructor(config, serialOnly) {
87928
+ this.config = config;
87929
+ this.serialOnly = serialOnly;
87930
+ this.buffer = [];
87931
+ this.bufferChars = 0;
87932
+ this.tail = Promise.resolve();
87933
+ }
87934
+ push(msg, handler) {
87935
+ var _a;
87936
+ this.buffer.push(msg);
87937
+ this.bufferChars += msg.content.length;
87938
+ (_a = this.pendingHandler) !== null && _a !== void 0 ? _a : (this.pendingHandler = handler);
87939
+ // Force flush when caps reached.
87940
+ if (this.buffer.length >= this.config.maxMessages
87941
+ || this.bufferChars >= this.config.maxChars) {
87942
+ this.clearTimer();
87943
+ this.enqueueFlush();
87944
+ return;
87945
+ }
87946
+ // Pure-serial mode (debounce disabled).
87947
+ if (this.config.delayMs <= 0 || this.serialOnly) {
87948
+ this.clearTimer();
87949
+ this.enqueueFlush();
87950
+ return;
87951
+ }
87952
+ // Debounced flush.
87953
+ this.clearTimer();
87954
+ const delay = this.bufferChars >= this.config.longThresholdChars
87955
+ ? this.config.longDelayMs
87956
+ : this.config.delayMs;
87957
+ this.timer = setTimeout(() => {
87958
+ this.timer = undefined;
87959
+ this.enqueueFlush();
87960
+ }, delay);
87961
+ }
87962
+ run(task) {
87963
+ // Any pending batch flushes first so a follow-on action-like task
87964
+ // runs after its chat's in-flight message work.
87965
+ if (this.buffer.length > 0) {
87966
+ this.clearTimer();
87967
+ this.enqueueFlush();
87968
+ }
87969
+ const next = this.tail.then(task, task);
87970
+ this.tail = next.then(() => undefined, () => undefined);
87971
+ return next;
87972
+ }
87973
+ flushNow() {
87974
+ return __awaiter(this, void 0, void 0, function* () {
87975
+ if (this.buffer.length > 0) {
87976
+ this.clearTimer();
87977
+ this.enqueueFlush();
87978
+ }
87979
+ yield this.tail;
87980
+ });
87981
+ }
87982
+ isIdle() {
87983
+ return this.buffer.length === 0 && !this.timer;
87984
+ }
87985
+ dispose() {
87986
+ this.clearTimer();
87987
+ this.buffer = [];
87988
+ this.pendingHandler = undefined;
87989
+ }
87990
+ clearTimer() {
87991
+ if (this.timer) {
87992
+ clearTimeout(this.timer);
87993
+ this.timer = undefined;
87994
+ }
87995
+ }
87996
+ enqueueFlush() {
87997
+ if (this.buffer.length === 0)
87998
+ return;
87999
+ const batch = this.buffer;
88000
+ const handler = this.pendingHandler;
88001
+ this.buffer = [];
88002
+ this.bufferChars = 0;
88003
+ this.pendingHandler = undefined;
88004
+ if (!handler)
88005
+ return;
88006
+ const dispatch = {
88007
+ message: mergeBatch(batch),
88008
+ sourceIds: batch.map((m) => m.messageId),
88009
+ };
88010
+ const task = () => handler(dispatch);
88011
+ const next = this.tail.then(task, task);
88012
+ this.tail = next.then(() => undefined, () => undefined);
88013
+ }
88014
+ }
88015
+ class ChatPipelineManager {
88016
+ constructor(config) {
88017
+ this.config = config;
88018
+ this.pipelines = new Map();
88019
+ }
88020
+ push(scope, msg, handler) {
88021
+ this.getOrCreate(scope, false).push(msg, handler);
88022
+ }
88023
+ run(scope, task) {
88024
+ return this.getOrCreate(scope, true).run(task);
88025
+ }
88026
+ getOrCreate(scope, serialOnly) {
88027
+ let p = this.pipelines.get(scope);
88028
+ if (!p) {
88029
+ p = new ChatPipeline(this.config, serialOnly);
88030
+ this.pipelines.set(scope, p);
88031
+ }
88032
+ return p;
88033
+ }
88034
+ flushAll() {
88035
+ return __awaiter(this, void 0, void 0, function* () {
88036
+ yield Promise.all([...this.pipelines.values()].map((p) => p.flushNow()));
88037
+ });
88038
+ }
88039
+ dispose() {
88040
+ return __awaiter(this, void 0, void 0, function* () {
88041
+ yield this.flushAll();
88042
+ for (const p of this.pipelines.values())
88043
+ p.dispose();
88044
+ this.pipelines.clear();
88045
+ });
88046
+ }
88047
+ }
88048
+ /**
88049
+ * Merge a batch of NormalizedMessages (all from the same chat) into a
88050
+ * single representative message. Keeps the latest-arrival metadata and
88051
+ * unions content / resources / mentions.
88052
+ */
88053
+ function mergeBatch(batch) {
88054
+ if (batch.length === 1)
88055
+ return batch[0];
88056
+ const last = batch[batch.length - 1];
88057
+ const content = batch
88058
+ .map((m) => m.content)
88059
+ .filter((c) => c && c.length > 0)
88060
+ .join('\n\n');
88061
+ const resources = dedupBy(batch.flatMap((m) => m.resources), (r) => r.fileKey);
88062
+ const mentions = dedupBy(batch.flatMap((m) => m.mentions), (m) => { var _a; return (_a = m.openId) !== null && _a !== void 0 ? _a : m.key; });
88063
+ return Object.assign(Object.assign({}, last), { content,
88064
+ resources,
88065
+ mentions, mentionAll: batch.some((m) => m.mentionAll), mentionedBot: batch.some((m) => m.mentionedBot) });
88066
+ }
88067
+ function dedupBy(items, key) {
88068
+ const seen = new Set();
88069
+ const out = [];
88070
+ for (const item of items) {
88071
+ const k = key(item);
88072
+ if (k == null) {
88073
+ out.push(item);
88074
+ continue;
88075
+ }
88076
+ if (seen.has(k))
88077
+ continue;
88078
+ seen.add(k);
88079
+ out.push(item);
88080
+ }
88081
+ return out;
88082
+ }
88083
+
88084
+ const DEFAULT_BATCH = {
88085
+ delayMs: 600,
88086
+ longThresholdChars: 1000,
88087
+ longDelayMs: 2000,
88088
+ maxMessages: 8,
88089
+ maxChars: 4000,
88090
+ };
88091
+ const DEFAULT_DEDUP = {
88092
+ ttl: 12 * 3600000,
88093
+ maxEntries: 5000,
88094
+ sweepIntervalMs: 5 * 60000,
88095
+ namespace: 'channel:seen',
88096
+ };
88097
+ const DEFAULT_STALE_MS = 30 * 60000;
88098
+ const DEFAULT_LOCK_TTL_MS = 5 * 60000;
88099
+ function resolveBatchConfig(cfg) {
88100
+ var _a, _b, _c, _d, _e, _f, _g;
88101
+ const t = (_b = (_a = cfg === null || cfg === void 0 ? void 0 : cfg.batch) === null || _a === void 0 ? void 0 : _a.text) !== null && _b !== void 0 ? _b : {};
88102
+ return {
88103
+ delayMs: (_c = t.delayMs) !== null && _c !== void 0 ? _c : DEFAULT_BATCH.delayMs,
88104
+ longThresholdChars: (_d = t.longThresholdChars) !== null && _d !== void 0 ? _d : DEFAULT_BATCH.longThresholdChars,
88105
+ longDelayMs: (_e = t.longDelayMs) !== null && _e !== void 0 ? _e : DEFAULT_BATCH.longDelayMs,
88106
+ maxMessages: (_f = t.maxMessages) !== null && _f !== void 0 ? _f : DEFAULT_BATCH.maxMessages,
88107
+ maxChars: (_g = t.maxChars) !== null && _g !== void 0 ? _g : DEFAULT_BATCH.maxChars,
88108
+ };
88109
+ }
88110
+
88111
+ /**
88112
+ * Two-tier dedup cache: hot in-memory LRU + injectable long-term Cache.
88113
+ * Memory tier serves 99% of queries sub-millisecond; long-term tier
88114
+ * survives process restarts when a persistent Cache implementation is
88115
+ * provided. Default node-sdk `internalCache` is memory-only — callers
88116
+ * wanting cross-restart persistence inject their own Cache (e.g., Redis).
88117
+ */
88118
+ class SeenCache {
88119
+ constructor(cache, opts = {}) {
88120
+ var _a, _b, _c, _d, _e, _f;
88121
+ this.cache = cache;
88122
+ this.memory = new Map(); // id → expireAt
88123
+ this.ttlMs = (_a = opts.ttlMs) !== null && _a !== void 0 ? _a : DEFAULT_DEDUP.ttl;
88124
+ this.maxMem = (_b = opts.maxMemEntries) !== null && _b !== void 0 ? _b : DEFAULT_DEDUP.maxEntries;
88125
+ this.ns = (_c = opts.namespace) !== null && _c !== void 0 ? _c : DEFAULT_DEDUP.namespace;
88126
+ const sweepMs = (_d = opts.sweepMs) !== null && _d !== void 0 ? _d : DEFAULT_DEDUP.sweepIntervalMs;
88127
+ this.sweeper = setInterval(() => this.sweep(), sweepMs);
88128
+ (_f = (_e = this.sweeper).unref) === null || _f === void 0 ? void 0 : _f.call(_e);
88129
+ }
88130
+ has(id) {
88131
+ return __awaiter(this, void 0, void 0, function* () {
88132
+ const now = Date.now();
88133
+ const exp = this.memory.get(id);
88134
+ if (exp && exp > now) {
88135
+ // refresh LRU position
88136
+ this.memory.delete(id);
88137
+ this.memory.set(id, exp);
88138
+ return true;
88139
+ }
88140
+ // fall through to long-term tier
88141
+ const hit = yield this.cache.get(id, { namespace: this.ns });
88142
+ if (hit) {
88143
+ this.memory.set(id, now + this.ttlMs);
88144
+ this.evictIfNeeded();
88145
+ return true;
88146
+ }
88147
+ return false;
88148
+ });
88149
+ }
88150
+ add(id) {
88151
+ return __awaiter(this, void 0, void 0, function* () {
88152
+ const expireAt = Date.now() + this.ttlMs;
88153
+ this.memory.set(id, expireAt);
88154
+ this.evictIfNeeded();
88155
+ // best-effort write to long-term; ignore failures
88156
+ try {
88157
+ yield this.cache.set(id, '1', expireAt, { namespace: this.ns });
88158
+ }
88159
+ catch (_a) {
88160
+ // tolerated — memory tier remains authoritative during the session
88161
+ }
88162
+ });
88163
+ }
88164
+ evictIfNeeded() {
88165
+ while (this.memory.size > this.maxMem) {
88166
+ const first = this.memory.keys().next().value;
88167
+ if (first === undefined)
88168
+ break;
88169
+ this.memory.delete(first);
88170
+ }
88171
+ }
88172
+ sweep() {
88173
+ const now = Date.now();
88174
+ for (const [k, v] of this.memory) {
88175
+ if (v <= now)
88176
+ this.memory.delete(k);
88177
+ }
88178
+ }
88179
+ dispose() {
88180
+ clearInterval(this.sweeper);
88181
+ this.memory.clear();
88182
+ }
88183
+ }
88184
+
88185
+ class PolicyGate {
88186
+ constructor(cfg, bot) {
88187
+ this.cfg = Object.assign({}, (cfg !== null && cfg !== void 0 ? cfg : {}));
88188
+ this.bot = bot;
88189
+ }
88190
+ evaluate(msg) {
88191
+ if (msg.chatType === 'group')
88192
+ return this.evaluateGroup(msg);
88193
+ return this.evaluateDm(msg);
88194
+ }
88195
+ evaluateGroup(msg) {
88196
+ var _a, _b;
88197
+ const allow = this.cfg.groupAllowlist;
88198
+ if (allow && allow.length > 0 && !allow.includes(msg.chatId)) {
88199
+ return { allowed: false, reason: 'group_not_allowed' };
88200
+ }
88201
+ const requireMention = (_a = this.cfg.requireMention) !== null && _a !== void 0 ? _a : true;
88202
+ if (requireMention && !msg.mentionedBot) {
88203
+ return { allowed: false, reason: 'no_mention' };
88204
+ }
88205
+ if (msg.mentionAll && !((_b = this.cfg.respondToMentionAll) !== null && _b !== void 0 ? _b : false)) {
88206
+ return { allowed: false, reason: 'mention_all_blocked' };
88207
+ }
88208
+ return { allowed: true };
88209
+ }
88210
+ evaluateDm(msg) {
88211
+ var _a, _b;
88212
+ const mode = (_a = this.cfg.dmMode) !== null && _a !== void 0 ? _a : 'open';
88213
+ if (mode === 'disabled') {
88214
+ return { allowed: false, reason: 'dm_disabled' };
88215
+ }
88216
+ if (mode === 'allowlist') {
88217
+ const allow = (_b = this.cfg.dmAllowlist) !== null && _b !== void 0 ? _b : [];
88218
+ if (!allow.includes(msg.senderId)) {
88219
+ return { allowed: false, reason: 'sender_not_allowed' };
88220
+ }
88221
+ }
88222
+ // 'pair' mode is reserved for a future iteration; treat as open for now.
88223
+ return { allowed: true };
88224
+ }
88225
+ updateConfig(partial) {
88226
+ this.cfg = Object.assign(Object.assign({}, this.cfg), partial);
88227
+ }
88228
+ getConfig() {
88229
+ return this.cfg;
88230
+ }
88231
+ setBotIdentity(bot) {
88232
+ this.bot = bot;
88233
+ }
88234
+ getBotIdentity() {
88235
+ return this.bot;
88236
+ }
88237
+ }
88238
+
88239
+ /**
88240
+ * Short-TTL in-memory lock to prevent concurrent processing of the same
88241
+ * event — complements SeenCache by covering the "currently in flight"
88242
+ * window, during which the event is not yet committed to SeenCache.
88243
+ */
88244
+ class ProcessingLock {
88245
+ constructor(ttlMs = DEFAULT_LOCK_TTL_MS, sweepMs = 60000) {
88246
+ var _a, _b;
88247
+ this.ttlMs = ttlMs;
88248
+ this.locks = new Map(); // id → expireAt (ms)
88249
+ this.sweeper = setInterval(() => this.sweep(), sweepMs);
88250
+ (_b = (_a = this.sweeper).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
88251
+ }
88252
+ /** Returns true if the lock is acquired; false if already held. */
88253
+ acquire(id) {
88254
+ const now = Date.now();
88255
+ const exp = this.locks.get(id);
88256
+ if (exp && exp > now)
88257
+ return false;
88258
+ this.locks.set(id, now + this.ttlMs);
88259
+ return true;
88260
+ }
88261
+ release(id) {
88262
+ this.locks.delete(id);
88263
+ }
88264
+ sweep() {
88265
+ const now = Date.now();
88266
+ for (const [k, v] of this.locks) {
88267
+ if (v <= now)
88268
+ this.locks.delete(k);
88269
+ }
88270
+ }
88271
+ dispose() {
88272
+ clearInterval(this.sweeper);
88273
+ this.locks.clear();
88274
+ }
88275
+ }
88276
+
88277
+ function isStale(createTimeMs, windowMs = DEFAULT_STALE_MS) {
88278
+ if (!createTimeMs || !Number.isFinite(createTimeMs))
88279
+ return false;
88280
+ return Date.now() - createTimeMs > windowMs;
88281
+ }
88282
+
88283
+ /**
88284
+ * Pipeline entry facade for the channel's safety layer.
88285
+ *
88286
+ * Three tiers of protection, each targeting different event shapes:
88287
+ * - pushMessage: full pipeline (stale + dedup + policy + lock + batch + queue)
88288
+ * - pushAction: dedup + lock + queue — for card button clicks and doc comments
88289
+ * - pushLight: dedup only — for reactions
88290
+ */
88291
+ class SafetyPipeline {
88292
+ constructor(opts) {
88293
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
88294
+ this.logger = opts.logger;
88295
+ this.onReject = opts.onReject;
88296
+ this.onMessage = opts.onMessage;
88297
+ this.staleWindow = (_b = (_a = opts.config) === null || _a === void 0 ? void 0 : _a.staleMessageWindowMs) !== null && _b !== void 0 ? _b : DEFAULT_STALE_MS;
88298
+ this.queueEnabled = (_e = (_d = (_c = opts.config) === null || _c === void 0 ? void 0 : _c.chatQueue) === null || _d === void 0 ? void 0 : _d.enabled) !== null && _e !== void 0 ? _e : true;
88299
+ this.seenCache = new SeenCache(opts.cache, {
88300
+ ttlMs: (_g = (_f = opts.config) === null || _f === void 0 ? void 0 : _f.dedup) === null || _g === void 0 ? void 0 : _g.ttl,
88301
+ maxMemEntries: (_j = (_h = opts.config) === null || _h === void 0 ? void 0 : _h.dedup) === null || _j === void 0 ? void 0 : _j.maxEntries,
88302
+ sweepMs: (_l = (_k = opts.config) === null || _k === void 0 ? void 0 : _k.dedup) === null || _l === void 0 ? void 0 : _l.sweepIntervalMs,
88303
+ });
88304
+ this.lock = new ProcessingLock();
88305
+ this.policy = new PolicyGate(opts.policy, opts.botIdentity);
88306
+ this.manager = new ChatPipelineManager(resolveBatchConfig(opts.config));
88307
+ }
88308
+ // ─── tier 1: full pipeline for IM messages ─────────────
88309
+ pushMessage(msg) {
88310
+ var _a, _b, _c, _d, _e, _f, _g;
88311
+ return __awaiter(this, void 0, void 0, function* () {
88312
+ if (isStale(msg.createTime, this.staleWindow)) {
88313
+ (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `safety: drop stale message ${msg.messageId}`);
88314
+ return;
88315
+ }
88316
+ if (yield this.seenCache.has(msg.messageId)) {
88317
+ (_d = (_c = this.logger).debug) === null || _d === void 0 ? void 0 : _d.call(_c, `safety: drop duplicate message ${msg.messageId}`);
88318
+ return;
88319
+ }
88320
+ const decision = this.policy.evaluate(msg);
88321
+ if (!decision.allowed) {
88322
+ this.onReject({
88323
+ messageId: msg.messageId,
88324
+ chatId: msg.chatId,
88325
+ senderId: msg.senderId,
88326
+ reason: (_e = decision.reason) !== null && _e !== void 0 ? _e : 'group_not_allowed',
88327
+ });
88328
+ return;
88329
+ }
88330
+ if (!this.lock.acquire(msg.messageId)) {
88331
+ (_g = (_f = this.logger).debug) === null || _g === void 0 ? void 0 : _g.call(_f, `safety: drop in-flight message ${msg.messageId}`);
88332
+ return;
88333
+ }
88334
+ const dispatchHandler = (batch) => __awaiter(this, void 0, void 0, function* () {
88335
+ var _h, _j;
88336
+ try {
88337
+ yield this.onMessage(batch.message);
88338
+ }
88339
+ catch (e) {
88340
+ (_j = (_h = this.logger).error) === null || _j === void 0 ? void 0 : _j.call(_h, `safety: message handler threw`, e);
88341
+ }
88342
+ finally {
88343
+ for (const id of batch.sourceIds) {
88344
+ try {
88345
+ yield this.seenCache.add(id);
88346
+ }
88347
+ catch ( /* best effort */_k) { /* best effort */ }
88348
+ this.lock.release(id);
88349
+ }
88350
+ }
88351
+ });
88352
+ if (this.queueEnabled) {
88353
+ this.manager.push(msg.chatId, msg, dispatchHandler);
88354
+ }
88355
+ else {
88356
+ // queueing disabled: fire-and-forget, no batch either
88357
+ void dispatchHandler({ message: msg, sourceIds: [msg.messageId] });
88358
+ }
88359
+ });
88360
+ }
88361
+ // ─── tier 2: dedup + lock + queue for cardAction & comment ─────
88362
+ pushAction(eventId, queueScope, handler) {
88363
+ var _a, _b, _c, _d;
88364
+ return __awaiter(this, void 0, void 0, function* () {
88365
+ if (yield this.seenCache.has(eventId)) {
88366
+ (_b = (_a = this.logger).debug) === null || _b === void 0 ? void 0 : _b.call(_a, `safety: drop duplicate action ${eventId}`);
88367
+ return;
88368
+ }
88369
+ if (!this.lock.acquire(eventId)) {
88370
+ (_d = (_c = this.logger).debug) === null || _d === void 0 ? void 0 : _d.call(_c, `safety: drop in-flight action ${eventId}`);
88371
+ return;
88372
+ }
88373
+ const task = () => __awaiter(this, void 0, void 0, function* () {
88374
+ var _e, _f;
88375
+ try {
88376
+ yield handler();
88377
+ }
88378
+ catch (e) {
88379
+ (_f = (_e = this.logger).error) === null || _f === void 0 ? void 0 : _f.call(_e, `safety: action handler threw`, e);
88380
+ }
88381
+ finally {
88382
+ try {
88383
+ yield this.seenCache.add(eventId);
88384
+ }
88385
+ catch ( /* best effort */_g) { /* best effort */ }
88386
+ this.lock.release(eventId);
88387
+ }
88388
+ });
88389
+ if (this.queueEnabled) {
88390
+ yield this.manager.run(queueScope, task);
88391
+ }
88392
+ else {
88393
+ yield task();
88394
+ }
88395
+ });
88396
+ }
88397
+ // ─── tier 3: dedup only (reactions) ────────────────────
88398
+ pushLight(eventId, handler) {
88399
+ var _a, _b;
88400
+ return __awaiter(this, void 0, void 0, function* () {
88401
+ if (yield this.seenCache.has(eventId))
88402
+ return;
88403
+ yield this.seenCache.add(eventId);
88404
+ try {
88405
+ yield handler();
88406
+ }
88407
+ catch (e) {
88408
+ (_b = (_a = this.logger).warn) === null || _b === void 0 ? void 0 : _b.call(_a, `safety: light handler threw`, e);
88409
+ }
88410
+ });
88411
+ }
88412
+ // ─── runtime config ────────────────────────────────────
88413
+ updatePolicy(partial) {
88414
+ this.policy.updateConfig(partial);
88415
+ }
88416
+ getPolicy() {
88417
+ return this.policy.getConfig();
88418
+ }
88419
+ setBotIdentity(bot) {
88420
+ this.policy.setBotIdentity(bot);
88421
+ }
88422
+ dispose() {
88423
+ return __awaiter(this, void 0, void 0, function* () {
88424
+ yield this.manager.dispose();
88425
+ this.seenCache.dispose();
88426
+ this.lock.dispose();
88427
+ });
88428
+ }
88429
+ }
88430
+
88431
+ function isMentionAll(m) {
88432
+ return m.key === '@_all';
88433
+ }
88434
+ function extractMentions(raw, botOpenId) {
88435
+ var _a, _b, _c;
88436
+ const mentions = new Map();
88437
+ const mentionsByOpenId = new Map();
88438
+ const mentionList = [];
88439
+ let mentionAll = false;
88440
+ let mentionedBot = false;
88441
+ for (const m of raw !== null && raw !== void 0 ? raw : []) {
88442
+ if (isMentionAll(m)) {
88443
+ mentionAll = true;
88444
+ mentions.set(m.key, { key: m.key, name: m.name, isBot: false });
88445
+ continue;
88446
+ }
88447
+ const openId = (_b = (_a = m.id) === null || _a === void 0 ? void 0 : _a.open_id) !== null && _b !== void 0 ? _b : '';
88448
+ const userId = (_c = m.id) === null || _c === void 0 ? void 0 : _c.user_id;
88449
+ const isBot = Boolean(botOpenId && openId === botOpenId);
88450
+ if (isBot)
88451
+ mentionedBot = true;
88452
+ const info = {
88453
+ key: m.key,
88454
+ openId: openId || undefined,
88455
+ userId,
88456
+ name: m.name,
88457
+ isBot,
88458
+ };
88459
+ mentions.set(m.key, info);
88460
+ if (openId)
88461
+ mentionsByOpenId.set(openId, info);
88462
+ mentionList.push(info);
88463
+ }
88464
+ return { mentions, mentionsByOpenId, mentionList, mentionAll, mentionedBot };
88465
+ }
88466
+ /**
88467
+ * Second-pass: replace placeholder keys in `content` with human-readable names
88468
+ * (or strip bot mentions if configured).
88469
+ *
88470
+ * Must run AFTER all converters have done their work, because converters use
88471
+ * placeholder keys to defer resolution to this single point.
88472
+ */
88473
+ function resolveMentions(content, ctx) {
88474
+ if (!content || ctx.mentions.size === 0)
88475
+ return content;
88476
+ let out = content;
88477
+ for (const [key, info] of ctx.mentions) {
88478
+ if (info.isBot && ctx.stripBotMentions) {
88479
+ // Remove key plus one surrounding whitespace on either side.
88480
+ const re = new RegExp(`\\s?${escapeRegex(key)}\\s?`, 'g');
88481
+ out = out.replace(re, ' ');
88482
+ continue;
88483
+ }
88484
+ const replacement = info.name ? `@${info.name}` : key;
88485
+ out = out.split(key).join(replacement);
88486
+ }
88487
+ // Collapse any double-spaces introduced by bot-mention stripping.
88488
+ return out.replace(/[ \t]{2,}/g, ' ').trim();
88489
+ }
88490
+ function escapeRegex(s) {
88491
+ return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
88492
+ }
88493
+
88494
+ function safeParse(raw) {
88495
+ if (!raw)
88496
+ return undefined;
88497
+ try {
88498
+ return JSON.parse(raw);
88499
+ }
88500
+ catch (_a) {
88501
+ return undefined;
88502
+ }
88503
+ }
88504
+ function applyStyle(text, style) {
88505
+ if (!style || style.length === 0)
88506
+ return text;
88507
+ let out = text;
88508
+ if (style.includes('bold'))
88509
+ out = `**${out}**`;
88510
+ if (style.includes('italic'))
88511
+ out = `*${out}*`;
88512
+ if (style.includes('underline'))
88513
+ out = `<u>${out}</u>`;
88514
+ if (style.includes('lineThrough') || style.includes('strikethrough'))
88515
+ out = `~~${out}~~`;
88516
+ if (style.includes('codeInline') || style.includes('code'))
88517
+ out = `\`${out}\``;
88518
+ return out;
88519
+ }
88520
+ const LOCALE_PRIORITY = ['zh_cn', 'en_us', 'ja_jp'];
88521
+ function unwrapLocale(parsed) {
88522
+ if ('title' in parsed || 'content' in parsed) {
88523
+ return parsed;
88524
+ }
88525
+ for (const loc of LOCALE_PRIORITY) {
88526
+ const hit = parsed[loc];
88527
+ if (hit != null && typeof hit === 'object')
88528
+ return hit;
88529
+ }
88530
+ const firstKey = Object.keys(parsed)[0];
88531
+ if (firstKey) {
88532
+ const first = parsed[firstKey];
88533
+ if (first != null && typeof first === 'object')
88534
+ return first;
88535
+ }
88536
+ return undefined;
88537
+ }
88538
+ function formatDuration(ms) {
88539
+ if (ms == null || !Number.isFinite(ms) || ms < 0)
88540
+ return undefined;
88541
+ if (ms < 1000)
88542
+ return `${Math.round(ms)}ms`;
88543
+ if (ms % 1000 === 0)
88544
+ return `${ms / 1000}s`;
88545
+ return `${(ms / 1000).toFixed(1)}s`;
88546
+ }
88547
+ function millisToDatetime(ms) {
88548
+ if (ms == null)
88549
+ return undefined;
88550
+ const n = typeof ms === 'string' ? parseInt(ms, 10) : ms;
88551
+ if (!Number.isFinite(n) || n <= 0)
88552
+ return undefined;
88553
+ const d = new Date(n + 8 * 3600000);
88554
+ const y = d.getUTCFullYear();
88555
+ const mo = String(d.getUTCMonth() + 1).padStart(2, '0');
88556
+ const day = String(d.getUTCDate()).padStart(2, '0');
88557
+ const h = String(d.getUTCHours()).padStart(2, '0');
88558
+ const mi = String(d.getUTCMinutes()).padStart(2, '0');
88559
+ return `${y}-${mo}-${day} ${h}:${mi}`;
88560
+ }
88561
+ function formatRFC3339Beijing(ms) {
88562
+ const d = new Date(ms + 8 * 3600000);
88563
+ const y = d.getUTCFullYear();
88564
+ const mo = String(d.getUTCMonth() + 1).padStart(2, '0');
88565
+ const day = String(d.getUTCDate()).padStart(2, '0');
88566
+ const h = String(d.getUTCHours()).padStart(2, '0');
88567
+ const mi = String(d.getUTCMinutes()).padStart(2, '0');
88568
+ const s = String(d.getUTCSeconds()).padStart(2, '0');
88569
+ return `${y}-${mo}-${day}T${h}:${mi}:${s}+08:00`;
88570
+ }
88571
+ function indentLines(text, indent) {
88572
+ return text
88573
+ .split('\n')
88574
+ .map((line) => `${indent}${line}`)
88575
+ .join('\n');
88576
+ }
88577
+ function escapeAttr(s) {
88578
+ return s.replace(/"/g, '&quot;');
88579
+ }
88580
+
88581
+ const convertAudio = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88582
+ const parsed = safeParse(raw);
88583
+ const fileKey = parsed === null || parsed === void 0 ? void 0 : parsed.file_key;
88584
+ if (!fileKey)
88585
+ return { content: '[audio]', resources: [] };
88586
+ const duration = parsed === null || parsed === void 0 ? void 0 : parsed.duration;
88587
+ const durAttr = formatDuration(duration);
88588
+ const attr = durAttr ? ` duration="${durAttr}"` : '';
88589
+ const content = `<audio key="${fileKey}"${attr}/>`;
88590
+ const resources = [
88591
+ { type: 'audio', fileKey, durationMs: duration },
88592
+ ];
88593
+ return { content, resources };
88594
+ });
88595
+
88596
+ function formatCalendarInner(raw) {
88597
+ const parsed = safeParse(raw);
88598
+ if (!parsed)
88599
+ return '[calendar event]';
88600
+ const lines = [];
88601
+ if (parsed.summary)
88602
+ lines.push(`📅 ${parsed.summary}`);
88603
+ const start = millisToDatetime(parsed.start_time);
88604
+ const end = millisToDatetime(parsed.end_time);
88605
+ if (start && end)
88606
+ lines.push(`🕙 ${start} ~ ${end}`);
88607
+ else if (start)
88608
+ lines.push(`🕙 ${start}`);
88609
+ return lines.length > 0 ? lines.join('\n') : '[calendar event]';
88610
+ }
88611
+ const convertCalendar = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88612
+ return ({
88613
+ content: `<calendar_invite>\n${formatCalendarInner(raw)}\n</calendar_invite>`,
88614
+ resources: [],
88615
+ });
88616
+ });
88617
+ const convertGeneralCalendar = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88618
+ return ({
88619
+ content: `<calendar>\n${formatCalendarInner(raw)}\n</calendar>`,
88620
+ resources: [],
88621
+ });
88622
+ });
88623
+ const convertShareCalendarEvent = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88624
+ return ({
88625
+ content: `<calendar_share>\n${formatCalendarInner(raw)}\n</calendar_share>`,
88626
+ resources: [],
88627
+ });
88628
+ });
88629
+
88630
+ const convertFile = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88631
+ const parsed = safeParse(raw);
88632
+ const fileKey = parsed === null || parsed === void 0 ? void 0 : parsed.file_key;
88633
+ if (!fileKey)
88634
+ return { content: '[file]', resources: [] };
88635
+ const fileName = parsed === null || parsed === void 0 ? void 0 : parsed.file_name;
88636
+ const nameAttr = fileName ? ` name="${escapeAttr(fileName)}"` : '';
88637
+ const content = `<file key="${fileKey}"${nameAttr}/>`;
88638
+ const resources = [{ type: 'file', fileKey, fileName }];
88639
+ return { content, resources };
88640
+ });
88641
+
88642
+ const convertFolder = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88643
+ const parsed = safeParse(raw);
88644
+ const fileKey = parsed === null || parsed === void 0 ? void 0 : parsed.file_key;
88645
+ if (!fileKey)
88646
+ return { content: '[folder]', resources: [] };
88647
+ const nameAttr = (parsed === null || parsed === void 0 ? void 0 : parsed.file_name) ? ` name="${escapeAttr(parsed.file_name)}"` : '';
88648
+ return { content: `<folder key="${fileKey}"${nameAttr}/>`, resources: [] };
88649
+ });
88650
+
88651
+ const convertHongbao = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88652
+ const parsed = safeParse(raw);
88653
+ const textAttr = (parsed === null || parsed === void 0 ? void 0 : parsed.text) ? ` text="${escapeAttr(parsed.text)}"` : '';
88654
+ return { content: `<hongbao${textAttr}/>`, resources: [] };
88655
+ });
88656
+
88657
+ const convertImage = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88658
+ const parsed = safeParse(raw);
88659
+ const imageKey = parsed === null || parsed === void 0 ? void 0 : parsed.image_key;
88660
+ if (!imageKey)
88661
+ return { content: '[image]', resources: [] };
88662
+ const resources = [{ type: 'image', fileKey: imageKey }];
88663
+ return { content: `![image](${imageKey})`, resources };
88664
+ });
88665
+
88666
+ /**
88667
+ * Recursive walker that extracts human-readable text from a Feishu
88668
+ * interactive card JSON tree. Covers header titles, plain_text / lark_md
88669
+ * elements, button labels, form fields, notes, and commonly used nested
88670
+ * element types.
88671
+ */
88672
+ function walkCard(node) {
88673
+ const out = [];
88674
+ visit(node, out);
88675
+ // de-duplicate adjacent empties and collapse
88676
+ return out.filter((s) => s && s.trim().length > 0);
88677
+ }
88678
+ function visit(node, out) {
88679
+ if (node == null)
88680
+ return;
88681
+ if (typeof node === 'string' || typeof node === 'number' || typeof node === 'boolean')
88682
+ return;
88683
+ if (Array.isArray(node)) {
88684
+ for (const child of node)
88685
+ visit(child, out);
88686
+ return;
88687
+ }
88688
+ if (typeof node !== 'object')
88689
+ return;
88690
+ const obj = node;
88691
+ // tag: plain_text / lark_md / markdown → push content
88692
+ const tag = obj.tag;
88693
+ if (typeof tag === 'string' && (tag === 'plain_text' || tag === 'lark_md' || tag === 'markdown')) {
88694
+ if (typeof obj.content === 'string')
88695
+ out.push(obj.content);
88696
+ return;
88697
+ }
88698
+ // header.title
88699
+ if (obj.header && typeof obj.header === 'object') {
88700
+ const header = obj.header;
88701
+ if (header.title)
88702
+ visit(header.title, out);
88703
+ }
88704
+ // text / content on common elements (div, button, note, etc.)
88705
+ if (obj.text)
88706
+ visit(obj.text, out);
88707
+ // button / select option label (typed as button)
88708
+ if (typeof tag === 'string' && tag === 'button') {
88709
+ const text = obj.text;
88710
+ if (text)
88711
+ visit(text, out);
88712
+ }
88713
+ // form fields: label, placeholder, options
88714
+ if (obj.label)
88715
+ visit(obj.label, out);
88716
+ if (obj.placeholder)
88717
+ visit(obj.placeholder, out);
88718
+ if (Array.isArray(obj.options)) {
88719
+ for (const opt of obj.options) {
88720
+ const o = opt;
88721
+ if (o === null || o === void 0 ? void 0 : o.text)
88722
+ visit(o.text, out);
88723
+ }
88724
+ }
88725
+ // column / row containers
88726
+ if (Array.isArray(obj.elements))
88727
+ for (const el of obj.elements)
88728
+ visit(el, out);
88729
+ if (Array.isArray(obj.fields))
88730
+ for (const f of obj.fields)
88731
+ visit(f, out);
88732
+ if (Array.isArray(obj.actions))
88733
+ for (const a of obj.actions)
88734
+ visit(a, out);
88735
+ if (Array.isArray(obj.columns))
88736
+ for (const c of obj.columns)
88737
+ visit(c, out);
88738
+ // common nested shapes for v2 card body
88739
+ if (obj.body)
88740
+ visit(obj.body, out);
88741
+ }
88742
+
88743
+ const convertInteractive = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88744
+ const parsed = safeParse(raw);
88745
+ if (parsed == null || typeof parsed !== 'object') {
88746
+ return { content: '[interactive card]', resources: [] };
88747
+ }
88748
+ const pieces = walkCard(parsed);
88749
+ if (pieces.length === 0) {
88750
+ return { content: '[interactive card]', resources: [] };
88751
+ }
88752
+ // Dedup adjacent duplicates while preserving order
88753
+ const seen = new Set();
88754
+ const out = [];
88755
+ for (const p of pieces) {
88756
+ const key = p.trim();
88757
+ if (!key || seen.has(key))
88758
+ continue;
88759
+ seen.add(key);
88760
+ out.push(key);
88761
+ }
88762
+ return { content: out.join('\n'), resources: [] };
88763
+ });
88764
+
88765
+ const convertLocation = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88766
+ const parsed = safeParse(raw);
88767
+ const name = parsed === null || parsed === void 0 ? void 0 : parsed.name;
88768
+ const lat = parsed === null || parsed === void 0 ? void 0 : parsed.latitude;
88769
+ const lng = parsed === null || parsed === void 0 ? void 0 : parsed.longitude;
88770
+ const nameAttr = name ? ` name="${escapeAttr(name)}"` : '';
88771
+ const coordsAttr = lat && lng ? ` coords="lat:${lat},lng:${lng}"` : '';
88772
+ return { content: `<location${nameAttr}${coordsAttr}/>`, resources: [] };
88773
+ });
88774
+
88775
+ const MAX_ITEMS = 50;
88776
+ const convertMergeForward = (_raw, ctx) => __awaiter(void 0, void 0, void 0, function* () {
88777
+ var _a;
88778
+ const { messageId, fetchSubMessages, dispatch } = ctx;
88779
+ if (!fetchSubMessages || !dispatch) {
88780
+ return { content: '<forwarded_messages/>', resources: [] };
88781
+ }
88782
+ let items;
88783
+ try {
88784
+ items = yield fetchSubMessages(messageId);
88785
+ }
88786
+ catch (_b) {
88787
+ return { content: '<forwarded_messages/>', resources: [] };
88788
+ }
88789
+ if (!items || items.length === 0) {
88790
+ return { content: '<forwarded_messages/>', resources: [] };
88791
+ }
88792
+ const capped = items.slice(0, MAX_ITEMS);
88793
+ const truncated = items.length > MAX_ITEMS;
88794
+ // Pre-warm sender name cache in one batch call.
88795
+ if (ctx.batchResolveNames) {
88796
+ const senderIds = new Set();
88797
+ for (const it of capped) {
88798
+ const sid = (_a = it.sender) === null || _a === void 0 ? void 0 : _a.id;
88799
+ if (sid && it.message_id !== messageId)
88800
+ senderIds.add(sid);
88801
+ }
88802
+ if (senderIds.size > 0) {
88803
+ try {
88804
+ yield ctx.batchResolveNames([...senderIds]);
88805
+ }
88806
+ catch (_c) {
88807
+ // best effort
88808
+ }
88809
+ }
88810
+ }
88811
+ const childrenMap = buildChildrenMap(capped, messageId);
88812
+ const content = yield formatSubTree(messageId, childrenMap, ctx, truncated);
88813
+ return { content, resources: [] };
88814
+ });
88815
+ function buildChildrenMap(items, rootId) {
88816
+ var _a;
88817
+ const map = new Map();
88818
+ for (const it of items) {
88819
+ if (it.message_id === rootId && !it.upper_message_id)
88820
+ continue;
88821
+ const pid = (_a = it.upper_message_id) !== null && _a !== void 0 ? _a : rootId;
88822
+ let arr = map.get(pid);
88823
+ if (!arr) {
88824
+ arr = [];
88825
+ map.set(pid, arr);
88826
+ }
88827
+ arr.push(it);
88828
+ }
88829
+ for (const arr of map.values()) {
88830
+ arr.sort((a, b) => {
88831
+ var _a, _b;
88832
+ const ta = parseInt(String((_a = a.create_time) !== null && _a !== void 0 ? _a : '0'), 10);
88833
+ const tb = parseInt(String((_b = b.create_time) !== null && _b !== void 0 ? _b : '0'), 10);
88834
+ return ta - tb;
88835
+ });
88836
+ }
88837
+ return map;
88838
+ }
88839
+ function formatSubTree(parentId, map, ctx, truncated = false) {
88840
+ return __awaiter(this, void 0, void 0, function* () {
88841
+ const children = map.get(parentId);
88842
+ if (!children || children.length === 0)
88843
+ return '<forwarded_messages/>';
88844
+ const parts = [];
88845
+ for (const item of children) {
88846
+ try {
88847
+ const sub = yield renderItem(item, map, ctx);
88848
+ if (sub)
88849
+ parts.push(sub);
88850
+ }
88851
+ catch (_a) {
88852
+ // skip bad item
88853
+ }
88854
+ }
88855
+ if (parts.length === 0)
88856
+ return '<forwarded_messages/>';
88857
+ const body = parts.join('\n');
88858
+ const footer = truncated ? '\n... (truncated)' : '';
88859
+ return `<forwarded_messages>\n${body}${footer}\n</forwarded_messages>`;
88860
+ });
88861
+ }
88862
+ function renderItem(item, map, ctx) {
88863
+ var _a, _b, _c, _d, _e, _f, _g, _h;
88864
+ return __awaiter(this, void 0, void 0, function* () {
88865
+ const msgType = (_a = item.msg_type) !== null && _a !== void 0 ? _a : 'text';
88866
+ const senderId = (_c = (_b = item.sender) === null || _b === void 0 ? void 0 : _b.id) !== null && _c !== void 0 ? _c : 'unknown';
88867
+ const createMs = parseInt(String((_d = item.create_time) !== null && _d !== void 0 ? _d : '0'), 10);
88868
+ const timestamp = createMs > 0 ? formatRFC3339Beijing(createMs) : 'unknown';
88869
+ const displayName = (_f = (_e = ctx.resolveUserName) === null || _e === void 0 ? void 0 : _e.call(ctx, senderId)) !== null && _f !== void 0 ? _f : senderId;
88870
+ let content;
88871
+ if (msgType === 'merge_forward') {
88872
+ // Nested forward — recurse locally without another API call.
88873
+ const nestedId = item.message_id;
88874
+ content = nestedId ? yield formatSubTree(nestedId, map, ctx) : '<forwarded_messages/>';
88875
+ }
88876
+ else {
88877
+ const rawContent = (_h = (_g = item.body) === null || _g === void 0 ? void 0 : _g.content) !== null && _h !== void 0 ? _h : '{}';
88878
+ if (!ctx.dispatch) {
88879
+ content = rawContent;
88880
+ }
88881
+ else {
88882
+ const r = yield ctx.dispatch(rawContent, msgType, ctx);
88883
+ content = r.content;
88884
+ }
88885
+ }
88886
+ const indented = indentLines(content, ' ');
88887
+ return `[${timestamp}] ${displayName}:\n${indented}`;
88888
+ });
88889
+ }
88890
+
88891
+ const convertPost = (raw, ctx) => __awaiter(void 0, void 0, void 0, function* () {
88892
+ var _a;
88893
+ const rawParsed = safeParse(raw);
88894
+ if (rawParsed == null || typeof rawParsed !== 'object') {
88895
+ return { content: '[rich text message]', resources: [] };
88896
+ }
88897
+ const body = unwrapLocale(rawParsed);
88898
+ if (!body)
88899
+ return { content: '[rich text message]', resources: [] };
88900
+ const resources = [];
88901
+ const lines = [];
88902
+ if (body.title) {
88903
+ lines.push(`**${body.title}**`);
88904
+ lines.push('');
88905
+ }
88906
+ for (const paragraph of (_a = body.content) !== null && _a !== void 0 ? _a : []) {
88907
+ if (!Array.isArray(paragraph))
88908
+ continue;
88909
+ let line = '';
88910
+ for (const el of paragraph) {
88911
+ line += renderElement(el, ctx, resources);
88912
+ }
88913
+ lines.push(line);
88914
+ }
88915
+ const content = lines.join('\n').trim() || '[rich text message]';
88916
+ return { content, resources };
88917
+ });
88918
+ function renderElement(el, ctx, resources) {
88919
+ var _a, _b, _c, _d, _e, _f, _g;
88920
+ switch (el.tag) {
88921
+ case 'text':
88922
+ return applyStyle((_a = el.text) !== null && _a !== void 0 ? _a : '', el.style);
88923
+ case 'a': {
88924
+ const label = (_c = (_b = el.text) !== null && _b !== void 0 ? _b : el.href) !== null && _c !== void 0 ? _c : '';
88925
+ return el.href ? `[${label}](${el.href})` : label;
88926
+ }
88927
+ case 'at': {
88928
+ const userId = (_d = el.user_id) !== null && _d !== void 0 ? _d : '';
88929
+ if (userId === 'all' || userId === 'all_members')
88930
+ return '@all';
88931
+ // Prefer placeholder key so resolveMentions handles it uniformly
88932
+ const info = ctx.mentionsByOpenId.get(userId);
88933
+ if (info)
88934
+ return info.key;
88935
+ return el.user_name ? `@${el.user_name}` : `@${userId}`;
88936
+ }
88937
+ case 'img': {
88938
+ if (el.image_key) {
88939
+ resources.push({ type: 'image', fileKey: el.image_key });
88940
+ return `![image](${el.image_key})`;
88941
+ }
88942
+ return '';
88943
+ }
88944
+ case 'media': {
88945
+ if (el.file_key) {
88946
+ resources.push({ type: 'file', fileKey: el.file_key });
88947
+ return `<file key="${el.file_key}"/>`;
88948
+ }
88949
+ return '';
88950
+ }
88951
+ case 'code_block': {
88952
+ const lang = (_e = el.language) !== null && _e !== void 0 ? _e : '';
88953
+ const code = (_f = el.text) !== null && _f !== void 0 ? _f : '';
88954
+ return `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
88955
+ }
88956
+ case 'hr':
88957
+ return '\n---\n';
88958
+ default:
88959
+ return (_g = el.text) !== null && _g !== void 0 ? _g : '';
88960
+ }
88961
+ }
88962
+
88963
+ const convertShareChat = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88964
+ var _a;
88965
+ const parsed = safeParse(raw);
88966
+ return {
88967
+ content: `<group_card id="${(_a = parsed === null || parsed === void 0 ? void 0 : parsed.chat_id) !== null && _a !== void 0 ? _a : ''}"/>`,
88968
+ resources: [],
88969
+ };
88970
+ });
88971
+ const convertShareUser = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88972
+ var _b;
88973
+ const parsed = safeParse(raw);
88974
+ return {
88975
+ content: `<contact_card id="${(_b = parsed === null || parsed === void 0 ? void 0 : parsed.user_id) !== null && _b !== void 0 ? _b : ''}"/>`,
88976
+ resources: [],
88977
+ };
88978
+ });
88979
+
88980
+ const convertSticker = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88981
+ const parsed = safeParse(raw);
88982
+ const fileKey = parsed === null || parsed === void 0 ? void 0 : parsed.file_key;
88983
+ if (!fileKey)
88984
+ return { content: '[sticker]', resources: [] };
88985
+ const resources = [{ type: 'sticker', fileKey }];
88986
+ return { content: `<sticker key="${fileKey}"/>`, resources };
88987
+ });
88988
+
88989
+ const convertSystem = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
88990
+ const parsed = safeParse(raw);
88991
+ if (!parsed || !parsed.template) {
88992
+ return { content: '[system message]', resources: [] };
88993
+ }
88994
+ const out = parsed.template.replace(/\{([a-z_]+)\}/g, (match, name) => {
88995
+ const val = parsed[name];
88996
+ if (Array.isArray(val))
88997
+ return val.join(', ');
88998
+ if (typeof val === 'string')
88999
+ return val;
89000
+ if (val == null)
89001
+ return '';
89002
+ return match;
89003
+ });
89004
+ return { content: out.trim() || '[system message]', resources: [] };
89005
+ });
89006
+
89007
+ const convertText = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
89008
+ var _a;
89009
+ const parsed = safeParse(raw);
89010
+ return { content: (_a = parsed === null || parsed === void 0 ? void 0 : parsed.text) !== null && _a !== void 0 ? _a : '', resources: [] };
89011
+ });
89012
+
89013
+ const convertTodo = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
89014
+ const parsed = safeParse(raw);
89015
+ if (!(parsed === null || parsed === void 0 ? void 0 : parsed.summary))
89016
+ return { content: '<todo>\n[todo]\n</todo>', resources: [] };
89017
+ const lines = [];
89018
+ if (parsed.summary.title)
89019
+ lines.push(parsed.summary.title);
89020
+ const bodyText = extractPostPlainText(parsed.summary.content);
89021
+ if (bodyText)
89022
+ lines.push(bodyText);
89023
+ const due = millisToDatetime(parsed.due_time);
89024
+ if (due)
89025
+ lines.push(`Due: ${due}`);
89026
+ if (lines.length === 0)
89027
+ return { content: '<todo>\n[todo]\n</todo>', resources: [] };
89028
+ return { content: `<todo>\n${lines.join('\n')}\n</todo>`, resources: [] };
89029
+ });
89030
+ function extractPostPlainText(blocks) {
89031
+ if (!blocks)
89032
+ return '';
89033
+ const lines = [];
89034
+ for (const paragraph of blocks) {
89035
+ if (!Array.isArray(paragraph))
89036
+ continue;
89037
+ const parts = [];
89038
+ for (const el of paragraph) {
89039
+ if (el.tag === 'text' && el.text)
89040
+ parts.push(el.text);
89041
+ else if (el.tag === 'a' && el.text)
89042
+ parts.push(el.text);
89043
+ }
89044
+ if (parts.length > 0)
89045
+ lines.push(parts.join(''));
89046
+ }
89047
+ return lines.join('\n');
89048
+ }
89049
+
89050
+ const convertUnknown = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
89051
+ const parsed = safeParse(raw);
89052
+ if (parsed && typeof parsed.text === 'string') {
89053
+ return { content: parsed.text, resources: [] };
89054
+ }
89055
+ return { content: '[unsupported message]', resources: [] };
89056
+ });
89057
+
89058
+ const convertVideo = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
89059
+ const parsed = safeParse(raw);
89060
+ const fileKey = parsed === null || parsed === void 0 ? void 0 : parsed.file_key;
89061
+ if (!fileKey)
89062
+ return { content: '[video]', resources: [] };
89063
+ const nameAttr = (parsed === null || parsed === void 0 ? void 0 : parsed.file_name) ? ` name="${escapeAttr(parsed.file_name)}"` : '';
89064
+ const durStr = formatDuration(parsed === null || parsed === void 0 ? void 0 : parsed.duration);
89065
+ const durAttr = durStr ? ` duration="${durStr}"` : '';
89066
+ const content = `<video key="${fileKey}"${nameAttr}${durAttr}/>`;
89067
+ const resources = [
89068
+ {
89069
+ type: 'video',
89070
+ fileKey,
89071
+ fileName: parsed === null || parsed === void 0 ? void 0 : parsed.file_name,
89072
+ durationMs: parsed === null || parsed === void 0 ? void 0 : parsed.duration,
89073
+ coverImageKey: parsed === null || parsed === void 0 ? void 0 : parsed.image_key,
89074
+ },
89075
+ ];
89076
+ return { content, resources };
89077
+ });
89078
+
89079
+ const convertVideoChat = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
89080
+ const parsed = safeParse(raw);
89081
+ if (!parsed) {
89082
+ return { content: '<meeting>\n[video chat]\n</meeting>', resources: [] };
89083
+ }
89084
+ const lines = [];
89085
+ if (parsed.topic)
89086
+ lines.push(`📹 ${parsed.topic}`);
89087
+ const start = millisToDatetime(parsed.start_time);
89088
+ if (start)
89089
+ lines.push(`🕙 ${start}`);
89090
+ const inner = lines.length > 0 ? lines.join('\n') : '[video chat]';
89091
+ return { content: `<meeting>\n${inner}\n</meeting>`, resources: [] };
89092
+ });
89093
+
89094
+ const convertVote = (raw, _ctx) => __awaiter(void 0, void 0, void 0, function* () {
89095
+ var _a, _b;
89096
+ const parsed = safeParse(raw);
89097
+ if (!parsed || (!parsed.topic && !((_a = parsed.options) === null || _a === void 0 ? void 0 : _a.length))) {
89098
+ return { content: '<vote>\n[vote]\n</vote>', resources: [] };
89099
+ }
89100
+ const lines = [];
89101
+ if (parsed.topic)
89102
+ lines.push(parsed.topic);
89103
+ for (const opt of (_b = parsed.options) !== null && _b !== void 0 ? _b : [])
89104
+ lines.push(`• ${opt}`);
89105
+ return { content: `<vote>\n${lines.join('\n')}\n</vote>`, resources: [] };
89106
+ });
89107
+
89108
+ const converters = new Map([
89109
+ ['text', convertText],
89110
+ ['post', convertPost],
89111
+ ['image', convertImage],
89112
+ ['file', convertFile],
89113
+ ['audio', convertAudio],
89114
+ ['video', convertVideo],
89115
+ ['media', convertVideo],
89116
+ ['sticker', convertSticker],
89117
+ ['interactive', convertInteractive],
89118
+ ['merge_forward', convertMergeForward],
89119
+ ['share_chat', convertShareChat],
89120
+ ['share_user', convertShareUser],
89121
+ ['location', convertLocation],
89122
+ ['system', convertSystem],
89123
+ ['vote', convertVote],
89124
+ ['todo', convertTodo],
89125
+ ['calendar', convertCalendar],
89126
+ ['general_calendar', convertGeneralCalendar],
89127
+ ['share_calendar_event', convertShareCalendarEvent],
89128
+ ['folder', convertFolder],
89129
+ ['hongbao', convertHongbao],
89130
+ ['video_chat', convertVideoChat],
89131
+ ]);
89132
+ /**
89133
+ * Dispatch a message content to the matching converter, with uniform error
89134
+ * containment — any thrown error is trapped and the fallback converter is
89135
+ * invoked instead so that normalization never fails catastrophically.
89136
+ */
89137
+ function dispatchConvert(raw, msgType, ctx) {
89138
+ var _a;
89139
+ return __awaiter(this, void 0, void 0, function* () {
89140
+ const fn = (_a = converters.get(msgType)) !== null && _a !== void 0 ? _a : convertUnknown;
89141
+ try {
89142
+ return yield fn(raw, ctx);
89143
+ }
89144
+ catch (_b) {
89145
+ return convertUnknown(raw);
89146
+ }
89147
+ });
89148
+ }
89149
+
89150
+ function normalizeCardAction(event) {
89151
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m;
89152
+ const messageId = (_b = (_a = event.context) === null || _a === void 0 ? void 0 : _a.open_message_id) !== null && _b !== void 0 ? _b : event.open_message_id;
89153
+ const chatId = (_d = (_c = event.context) === null || _c === void 0 ? void 0 : _c.open_chat_id) !== null && _d !== void 0 ? _d : event.open_chat_id;
89154
+ const operatorOpenId = (_e = event.operator) === null || _e === void 0 ? void 0 : _e.open_id;
89155
+ if (!messageId || !chatId || !operatorOpenId)
89156
+ return null;
89157
+ return {
89158
+ messageId,
89159
+ chatId,
89160
+ operator: {
89161
+ openId: operatorOpenId,
89162
+ userId: (_f = event.operator) === null || _f === void 0 ? void 0 : _f.user_id,
89163
+ name: (_g = event.operator) === null || _g === void 0 ? void 0 : _g.name,
89164
+ },
89165
+ action: {
89166
+ value: (_h = event.action) === null || _h === void 0 ? void 0 : _h.value,
89167
+ tag: (_k = (_j = event.action) === null || _j === void 0 ? void 0 : _j.tag) !== null && _k !== void 0 ? _k : 'unknown',
89168
+ name: (_l = event.action) === null || _l === void 0 ? void 0 : _l.name,
89169
+ option: (_m = event.action) === null || _m === void 0 ? void 0 : _m.option,
89170
+ },
89171
+ };
89172
+ }
89173
+
89174
+ function normalizeReaction(event, action) {
89175
+ var _a, _b, _c, _d;
89176
+ const messageId = event.message_id;
89177
+ const emojiType = (_a = event.reaction_type) === null || _a === void 0 ? void 0 : _a.emoji_type;
89178
+ const operatorOpenId = (_b = event.user_id) === null || _b === void 0 ? void 0 : _b.open_id;
89179
+ if (!messageId || !emojiType || !operatorOpenId)
89180
+ return null;
89181
+ const actionTimeStr = event.action_time;
89182
+ const actionTime = actionTimeStr ? parseInt(actionTimeStr, 10) : undefined;
89183
+ return {
89184
+ messageId,
89185
+ operator: {
89186
+ openId: operatorOpenId,
89187
+ userId: (_d = (_c = event.user_id) === null || _c === void 0 ? void 0 : _c.user_id) !== null && _d !== void 0 ? _d : undefined,
89188
+ },
89189
+ emojiType,
89190
+ action,
89191
+ actionTime: actionTime != null && Number.isFinite(actionTime) ? actionTime : undefined,
89192
+ };
89193
+ }
89194
+
89195
+ function normalizeBotAdded(event, opts) {
89196
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
89197
+ const chatId = event.chat_id;
89198
+ const operatorOpenId = (_a = event.operator_id) === null || _a === void 0 ? void 0 : _a.open_id;
89199
+ if (!chatId || !operatorOpenId)
89200
+ return null;
89201
+ const botName = (_f = (_d = (_b = event.name) !== null && _b !== void 0 ? _b : (_c = event.i18n_names) === null || _c === void 0 ? void 0 : _c.zh_cn) !== null && _d !== void 0 ? _d : (_e = event.i18n_names) === null || _e === void 0 ? void 0 : _e.en_us) !== null && _f !== void 0 ? _f : (_g = event.i18n_names) === null || _g === void 0 ? void 0 : _g.ja_jp;
89202
+ return {
89203
+ chatId,
89204
+ operator: {
89205
+ openId: operatorOpenId,
89206
+ userId: (_j = (_h = event.operator_id) === null || _h === void 0 ? void 0 : _h.user_id) !== null && _j !== void 0 ? _j : undefined,
89207
+ },
89208
+ botName,
89209
+ external: event.external,
89210
+ raw: (opts === null || opts === void 0 ? void 0 : opts.includeRaw) ? event : undefined,
89211
+ };
89212
+ }
89213
+
89214
+ function normalizeComment(event, opts) {
89215
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o;
89216
+ const fileToken = (_a = event.file_token) !== null && _a !== void 0 ? _a : (_b = event.notice_meta) === null || _b === void 0 ? void 0 : _b.file_token;
89217
+ const fileType = (_c = event.file_type) !== null && _c !== void 0 ? _c : (_d = event.notice_meta) === null || _d === void 0 ? void 0 : _d.file_type;
89218
+ const commentId = event.comment_id;
89219
+ const userId = (_f = (_e = event.notice_meta) === null || _e === void 0 ? void 0 : _e.from_user_id) !== null && _f !== void 0 ? _f : event.user_id;
89220
+ const operatorOpenId = userId === null || userId === void 0 ? void 0 : userId.open_id;
89221
+ if (!fileToken || !fileType || !commentId || !operatorOpenId)
89222
+ return null;
89223
+ const tsStr = (_j = (_g = event.create_time) !== null && _g !== void 0 ? _g : (_h = event.notice_meta) === null || _h === void 0 ? void 0 : _h.timestamp) !== null && _j !== void 0 ? _j : event.action_time;
89224
+ const timestamp = tsStr ? parseInt(tsStr, 10) : Date.now();
89225
+ return {
89226
+ fileToken,
89227
+ fileType,
89228
+ commentId,
89229
+ replyId: event.reply_id,
89230
+ operator: {
89231
+ openId: operatorOpenId,
89232
+ userId: (_k = userId === null || userId === void 0 ? void 0 : userId.user_id) !== null && _k !== void 0 ? _k : undefined,
89233
+ unionId: userId === null || userId === void 0 ? void 0 : userId.union_id,
89234
+ },
89235
+ mentionedBot: Boolean((_o = (_l = event.is_mentioned) !== null && _l !== void 0 ? _l : (_m = event.notice_meta) === null || _m === void 0 ? void 0 : _m.is_mentioned) !== null && _o !== void 0 ? _o : event.is_mention),
89236
+ timestamp: Number.isFinite(timestamp) ? timestamp : Date.now(),
89237
+ raw: (opts === null || opts === void 0 ? void 0 : opts.includeRaw) ? event : undefined,
89238
+ };
89239
+ }
89240
+
89241
+ /**
89242
+ * Normalize a raw Feishu message event into a NormalizedMessage.
89243
+ *
89244
+ * Pipeline:
89245
+ * 1. Extract mentions → build key/openId maps + bot detection
89246
+ * 2. For `interactive` type, fetch full v2 card content if capability available
89247
+ * 3. Build ConvertContext with injected capabilities
89248
+ * 4. Dispatch to the matching converter (uniform error containment inside)
89249
+ * 5. Run resolveMentions second pass — replace placeholders with @name
89250
+ * 6. Assemble and return NormalizedMessage
89251
+ */
89252
+ function normalize(event, opts) {
89253
+ var _a, _b, _c, _d, _e;
89254
+ return __awaiter(this, void 0, void 0, function* () {
89255
+ const msg = event.message;
89256
+ const botOpenId = (_a = opts.botIdentity) === null || _a === void 0 ? void 0 : _a.openId;
89257
+ const { mentions, mentionsByOpenId, mentionList, mentionAll: mentionAllFromRaw, mentionedBot } = extractMentions(msg.mentions, botOpenId);
89258
+ // Feishu frequently omits the `mentions` array even when the message
89259
+ // contains `@所有人` — the placeholder `@_all` stays inline in content.
89260
+ // Fall back to a content-level scan so policy gating (respondToMentionAll)
89261
+ // and downstream consumers see a truthy mentionAll in that case.
89262
+ const mentionAll = mentionAllFromRaw || detectMentionAllInContent(msg.content);
89263
+ const ctx = {
89264
+ messageId: msg.message_id,
89265
+ botOpenId,
89266
+ mentions,
89267
+ mentionsByOpenId,
89268
+ stripBotMentions: (_b = opts.stripBotMentions) !== null && _b !== void 0 ? _b : true,
89269
+ fetchSubMessages: opts.fetchSubMessages,
89270
+ resolveUserName: opts.resolveUserName,
89271
+ batchResolveNames: opts.batchResolveNames,
89272
+ dispatch: dispatchConvert,
89273
+ };
89274
+ const { content: rawContent, resources } = yield dispatchConvert(msg.content, msg.message_type, ctx);
89275
+ const content = resolveMentions(rawContent, ctx);
89276
+ const senderOpenId = event.sender.sender_id.open_id;
89277
+ const senderFallbackId = (_d = (_c = event.sender.sender_id.user_id) !== null && _c !== void 0 ? _c : event.sender.sender_id.union_id) !== null && _d !== void 0 ? _d : '';
89278
+ const senderId = senderOpenId !== null && senderOpenId !== void 0 ? senderOpenId : senderFallbackId;
89279
+ const senderName = senderOpenId ? (_e = opts.resolveSenderName) === null || _e === void 0 ? void 0 : _e.call(opts, senderOpenId) : undefined;
89280
+ const createMs = msg.create_time ? parseInt(msg.create_time, 10) : 0;
89281
+ return {
89282
+ messageId: msg.message_id,
89283
+ chatId: msg.chat_id,
89284
+ chatType: msg.chat_type,
89285
+ senderId,
89286
+ senderName,
89287
+ content,
89288
+ rawContentType: msg.message_type,
89289
+ resources,
89290
+ mentions: mentionList,
89291
+ mentionAll,
89292
+ mentionedBot,
89293
+ rootId: msg.root_id,
89294
+ threadId: msg.thread_id,
89295
+ replyToMessageId: msg.parent_id,
89296
+ createTime: Number.isFinite(createMs) ? createMs : 0,
89297
+ raw: opts.includeRaw ? event : undefined,
89298
+ };
89299
+ });
89300
+ }
89301
+ /**
89302
+ * Detect `@_all` placeholder inside a raw Feishu content JSON string without
89303
+ * parsing. We deliberately search the serialized form (not the parsed text),
89304
+ * because the placeholder can appear in a `text` field (text/post) or inside
89305
+ * nested content arrays (post). The placeholder is bounded by non-word chars
89306
+ * on the right (whitespace, quote, punctuation) — on the left `@` is already
89307
+ * a non-word char so no explicit boundary is needed.
89308
+ */
89309
+ function detectMentionAllInContent(content) {
89310
+ if (!content)
89311
+ return false;
89312
+ return /@_all\b/.test(content);
89313
+ }
89314
+
89315
+ class LarkChannel {
89316
+ constructor(opts) {
89317
+ var _a, _b, _c, _d, _e, _f, _g;
89318
+ this.handlers = {};
89319
+ this.connected = false;
89320
+ this.opts = opts;
89321
+ this.logger = new LoggerProxy((_a = opts.loggerLevel) !== null && _a !== void 0 ? _a : LoggerLevel.info, (_b = opts.logger) !== null && _b !== void 0 ? _b : defaultLogger);
89322
+ this.rawClient = new Client({
89323
+ appId: opts.appId,
89324
+ appSecret: opts.appSecret,
89325
+ domain: (_c = opts.domain) !== null && _c !== void 0 ? _c : Domain.Feishu,
89326
+ cache: opts.cache,
89327
+ httpInstance: opts.httpInstance,
89328
+ logger: opts.logger,
89329
+ loggerLevel: opts.loggerLevel,
89330
+ source: opts.source,
89331
+ });
89332
+ this.dispatcher = new EventDispatcher({
89333
+ verificationToken: (_d = opts.webhook) === null || _d === void 0 ? void 0 : _d.verificationToken,
89334
+ encryptKey: (_e = opts.webhook) === null || _e === void 0 ? void 0 : _e.encryptKey,
89335
+ cache: opts.cache,
89336
+ logger: opts.logger,
89337
+ loggerLevel: opts.loggerLevel,
89338
+ });
89339
+ this.sender = new OutboundSender(this.rawClient, (_f = opts.outbound) !== null && _f !== void 0 ? _f : {}, this.logger);
89340
+ this.safety = new SafetyPipeline({
89341
+ config: opts.safety,
89342
+ policy: opts.policy,
89343
+ cache: (_g = opts.cache) !== null && _g !== void 0 ? _g : internalCache,
89344
+ logger: this.logger,
89345
+ onReject: (evt) => { var _a, _b; (_b = (_a = this.handlers).reject) === null || _b === void 0 ? void 0 : _b.call(_a, evt); },
89346
+ onMessage: (merged) => __awaiter(this, void 0, void 0, function* () {
89347
+ const handler = this.handlers.message;
89348
+ if (handler)
89349
+ yield handler(merged);
89350
+ }),
89351
+ });
89352
+ }
89353
+ // ─── lifecycle ──────────────────────────────────────────
89354
+ connect() {
89355
+ return __awaiter(this, void 0, void 0, function* () {
89356
+ if (this.connectPromise)
89357
+ return this.connectPromise;
89358
+ this.connectPromise = this.doConnect().catch((err) => {
89359
+ this.connectPromise = undefined;
89360
+ throw err;
89361
+ });
89362
+ return this.connectPromise;
89363
+ });
89364
+ }
89365
+ doConnect() {
89366
+ var _a;
89367
+ return __awaiter(this, void 0, void 0, function* () {
89368
+ this.botIdentity = yield this.fetchBotIdentity();
89369
+ this.safety.setBotIdentity(this.botIdentity);
89370
+ this.registerDispatcherHandlers();
89371
+ const transport = (_a = this.opts.transport) !== null && _a !== void 0 ? _a : 'websocket';
89372
+ if (transport === 'websocket') {
89373
+ yield this.connectWebSocket(15000);
89374
+ }
89375
+ // webhook transport wiring is external: user plugs this.dispatcher into
89376
+ // their HTTP handler via the existing adaptor modules.
89377
+ this.connected = true;
89378
+ });
89379
+ }
89380
+ /**
89381
+ * Construct the underlying WSClient and wait for its `onReady` callback —
89382
+ * so `connect()` only resolves after the first WebSocket handshake
89383
+ * actually succeeds. Rejects on `onError` or if the handshake doesn't
89384
+ * complete within `timeoutMs`.
89385
+ *
89386
+ * Also wires `onReconnecting` / `onReconnected` callbacks to emit the
89387
+ * corresponding public events.
89388
+ */
89389
+ connectWebSocket(timeoutMs) {
89390
+ return new Promise((resolve, reject) => {
89391
+ var _a;
89392
+ let settled = false;
89393
+ const timer = setTimeout(() => {
89394
+ if (settled)
89395
+ return;
89396
+ settled = true;
89397
+ reject(new LarkChannelError('not_connected', `WebSocket handshake did not complete within ${timeoutMs}ms`));
89398
+ }, timeoutMs);
89399
+ this.rawWsClient = new WSClient({
89400
+ appId: this.opts.appId,
89401
+ appSecret: this.opts.appSecret,
89402
+ domain: (_a = this.opts.domain) !== null && _a !== void 0 ? _a : Domain.Feishu,
89403
+ logger: this.opts.logger,
89404
+ loggerLevel: this.opts.loggerLevel,
89405
+ httpInstance: this.opts.httpInstance,
89406
+ autoReconnect: true,
89407
+ source: this.opts.source,
89408
+ onReady: () => {
89409
+ if (settled)
89410
+ return;
89411
+ settled = true;
89412
+ clearTimeout(timer);
89413
+ resolve();
89414
+ },
89415
+ onError: (err) => {
89416
+ if (settled)
89417
+ return;
89418
+ settled = true;
89419
+ clearTimeout(timer);
89420
+ reject(new LarkChannelError('not_connected', `WebSocket connect failed: ${err.message}`, { cause: err }));
89421
+ },
89422
+ onReconnecting: () => { var _a, _b; return (_b = (_a = this.handlers).reconnecting) === null || _b === void 0 ? void 0 : _b.call(_a); },
89423
+ onReconnected: () => { var _a, _b; return (_b = (_a = this.handlers).reconnected) === null || _b === void 0 ? void 0 : _b.call(_a); },
89424
+ });
89425
+ this.rawWsClient.start({ eventDispatcher: this.dispatcher });
89426
+ });
89427
+ }
89428
+ disconnect() {
89429
+ var _a;
89430
+ return __awaiter(this, void 0, void 0, function* () {
89431
+ if (!this.connected)
89432
+ return;
89433
+ try {
89434
+ (_a = this.rawWsClient) === null || _a === void 0 ? void 0 : _a.close({});
89435
+ }
89436
+ catch ( /* best effort */_b) { /* best effort */ }
89437
+ try {
89438
+ yield this.safety.dispose();
89439
+ }
89440
+ catch ( /* best effort */_c) { /* best effort */ }
89441
+ this.connected = false;
89442
+ this.connectPromise = undefined;
89443
+ });
89444
+ }
89445
+ on(nameOrMap, handler) {
89446
+ if (typeof nameOrMap === 'string') {
89447
+ return this.attachSingle(nameOrMap, handler);
89448
+ }
89449
+ const unsubs = [];
89450
+ Object.keys(nameOrMap).forEach((k) => {
89451
+ const fn = nameOrMap[k];
89452
+ if (fn)
89453
+ unsubs.push(this.attachSingle(k, fn));
89454
+ });
89455
+ return () => unsubs.forEach((u) => u());
89456
+ }
89457
+ attachSingle(name, handler) {
89458
+ if (this.handlers[name]) {
89459
+ this.logger.warn(`channel: handler for "${name}" is being overwritten`);
89460
+ }
89461
+ this.handlers[name] = handler;
89462
+ return () => {
89463
+ if (this.handlers[name] === handler)
89464
+ delete this.handlers[name];
89465
+ };
89466
+ }
89467
+ // ─── outbound ──────────────────────────────────────────
89468
+ send(to, input, opts) {
89469
+ return __awaiter(this, void 0, void 0, function* () {
89470
+ return this.sender.send(to, input, opts);
89471
+ });
89472
+ }
89473
+ stream(to, input, opts) {
89474
+ return __awaiter(this, void 0, void 0, function* () {
89475
+ return this.sender.stream(to, input, opts);
89476
+ });
89477
+ }
89478
+ // ─── low-level ─────────────────────────────────────────
89479
+ updateCard(messageId, card) {
89480
+ return __awaiter(this, void 0, void 0, function* () {
89481
+ yield this.sender.patchCard(messageId, card);
89482
+ });
89483
+ }
89484
+ /**
89485
+ * Edit an already-sent message's text/post content. Uses `im.v1.message.update`
89486
+ * which (per Feishu docs) only supports editing text and rich-text (post)
89487
+ * messages. For cards, use {@link updateCard} instead — a wrong attempt to
89488
+ * use this on a card would hit the same API and fail with a clearer
89489
+ * Feishu-side error.
89490
+ */
89491
+ editMessage(messageId, text) {
89492
+ return __awaiter(this, void 0, void 0, function* () {
89493
+ yield this.rawClient.im.v1.message.update({
89494
+ path: { message_id: messageId },
89495
+ data: {
89496
+ msg_type: 'text',
89497
+ content: JSON.stringify({ text }),
89498
+ },
89499
+ });
89500
+ });
89501
+ }
89502
+ recallMessage(messageId) {
89503
+ return __awaiter(this, void 0, void 0, function* () {
89504
+ yield this.rawClient.im.v1.message.delete({
89505
+ path: { message_id: messageId },
89506
+ });
89507
+ });
89508
+ }
89509
+ /**
89510
+ * Add an emoji reaction to a message. Returns the `reaction_id` Feishu
89511
+ * assigned — stash it if you want to {@link removeReaction} later,
89512
+ * since the raw `im.message.reaction.*_v1` events don't carry the id.
89513
+ * Only the bot's own reactions can be removed.
89514
+ */
89515
+ addReaction(messageId, emojiType) {
89516
+ var _a, _b;
89517
+ return __awaiter(this, void 0, void 0, function* () {
89518
+ const r = yield this.rawClient.im.v1.messageReaction.create({
89519
+ path: { message_id: messageId },
89520
+ data: { reaction_type: { emoji_type: emojiType } },
89521
+ });
89522
+ const rid = (_b = (_a = r === null || r === void 0 ? void 0 : r.data) === null || _a === void 0 ? void 0 : _a.reaction_id) !== null && _b !== void 0 ? _b : r === null || r === void 0 ? void 0 : r.reaction_id;
89523
+ if (!rid) {
89524
+ throw new LarkChannelError('unknown', 'messageReaction.create returned no reaction_id');
89525
+ }
89526
+ return rid;
89527
+ });
89528
+ }
89529
+ /**
89530
+ * Remove a reaction by its `reaction_id` (the value returned from
89531
+ * {@link addReaction}). Only the bot's own reactions can be removed —
89532
+ * removing a user-added reaction will fail with a Feishu permission
89533
+ * error.
89534
+ */
89535
+ removeReaction(messageId, reactionId) {
89536
+ return __awaiter(this, void 0, void 0, function* () {
89537
+ yield this.rawClient.im.v1.messageReaction.delete({
89538
+ path: { message_id: messageId, reaction_id: reactionId },
89539
+ });
89540
+ });
89541
+ }
89542
+ /**
89543
+ * Convenience: remove the bot's reaction on `messageId` matching
89544
+ * `emojiType`, without needing the `reaction_id`. Lists the message's
89545
+ * reactions filtered by emoji, picks the one added by this bot
89546
+ * (operator_type === 'app'), and deletes it. Returns `true` if a
89547
+ * matching reaction was found and deleted, `false` otherwise (including
89548
+ * the case where the bot never added that emoji).
89549
+ */
89550
+ removeReactionByEmoji(messageId, emojiType) {
89551
+ var _a, _b;
89552
+ return __awaiter(this, void 0, void 0, function* () {
89553
+ const r = yield this.rawClient.im.v1.messageReaction.list({
89554
+ path: { message_id: messageId },
89555
+ params: { reaction_type: emojiType, page_size: 50 },
89556
+ });
89557
+ const items = (_b = (_a = r === null || r === void 0 ? void 0 : r.data) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : [];
89558
+ const mine = items.find((it) => { var _a; return ((_a = it.operator) === null || _a === void 0 ? void 0 : _a.operator_type) === 'app'; });
89559
+ if (!(mine === null || mine === void 0 ? void 0 : mine.reaction_id))
89560
+ return false;
89561
+ yield this.removeReaction(messageId, mine.reaction_id);
89562
+ return true;
89563
+ });
89564
+ }
89565
+ downloadResource(fileKey, type) {
89566
+ return __awaiter(this, void 0, void 0, function* () {
89567
+ if (type === 'image') {
89568
+ const r = yield this.rawClient.im.v1.image.get({
89569
+ path: { image_key: fileKey },
89570
+ });
89571
+ return yield bufferFromStream(r);
89572
+ }
89573
+ const r = yield this.rawClient.im.v1.file.get({
89574
+ path: { file_key: fileKey },
89575
+ });
89576
+ return yield bufferFromStream(r);
89577
+ });
89578
+ }
89579
+ getChatInfo(chatId) {
89580
+ var _a, _b;
89581
+ return __awaiter(this, void 0, void 0, function* () {
89582
+ const r = yield this.rawClient.im.v1.chat.get({
89583
+ path: { chat_id: chatId },
89584
+ });
89585
+ const d = (_a = r.data) !== null && _a !== void 0 ? _a : {};
89586
+ return {
89587
+ chatId,
89588
+ name: d.name,
89589
+ description: d.description,
89590
+ chatType: (_b = d.chat_mode) !== null && _b !== void 0 ? _b : 'group',
89591
+ ownerId: d.owner_id,
89592
+ memberCount: d.user_count,
89593
+ };
89594
+ });
89595
+ }
89596
+ // ─── runtime config ────────────────────────────────────
89597
+ updatePolicy(partial) {
89598
+ this.safety.updatePolicy(partial);
89599
+ }
89600
+ getPolicy() {
89601
+ return this.safety.getPolicy();
89602
+ }
89603
+ // ─── internals: bot identity & dispatch wiring ────────
89604
+ fetchBotIdentity() {
89605
+ var _a;
89606
+ return __awaiter(this, void 0, void 0, function* () {
89607
+ // Standard Feishu API: GET /open-apis/bot/v3/info
89608
+ // Returns: { code, msg, bot: { open_id, app_name, avatar_url, ... } }
89609
+ let lastError;
89610
+ try {
89611
+ const r = yield this.rawClient.request({
89612
+ url: '/open-apis/bot/v3/info',
89613
+ method: 'GET',
89614
+ });
89615
+ const bot = r.bot;
89616
+ if (bot === null || bot === void 0 ? void 0 : bot.open_id) {
89617
+ return { openId: bot.open_id, name: (_a = bot.app_name) !== null && _a !== void 0 ? _a : 'bot' };
89618
+ }
89619
+ lastError = new Error(`bot/v3/info response missing open_id: ${JSON.stringify(r).slice(0, 200)}`);
89620
+ }
89621
+ catch (e) {
89622
+ lastError = e;
89623
+ }
89624
+ // Let the shared error classifier decide: 401/403 / feishu auth codes
89625
+ // → permission_denied; rate_limited / send_timeout pass through;
89626
+ // everything else falls back to `not_connected` (the genuine
89627
+ // "couldn't reach the API" bucket). Without this, all connect
89628
+ // failures collapse to `not_connected`, making auth errors
89629
+ // indistinguishable from network errors.
89630
+ const classified = classifyError(lastError);
89631
+ const code = classified.code === 'unknown' ? 'not_connected' : classified.code;
89632
+ throw new LarkChannelError(code, 'could not resolve bot identity via /open-apis/bot/v3/info — required for channel to function', { cause: lastError });
89633
+ });
89634
+ }
89635
+ registerDispatcherHandlers() {
89636
+ // `im.v1.message.get(mid)` on a merge_forward message returns
89637
+ // `data.items[]` as a flat list: the parent message first (no
89638
+ // `upper_message_id`) followed by every descendant, each with
89639
+ // `upper_message_id` pointing at its direct parent. That is
89640
+ // exactly what `convertMergeForward` / `buildChildrenMap` consume,
89641
+ // so the converter tree-builds correctly without further work.
89642
+ // (Earlier attempts used `message.list` with
89643
+ // `container_id_type: 'message'`, which Feishu rejects — 'message'
89644
+ // isn't a valid container type.)
89645
+ const fetchSubMessages = (mid) => __awaiter(this, void 0, void 0, function* () {
89646
+ var _a, _b, _c, _d;
89647
+ try {
89648
+ const r = yield this.rawClient.im.v1.message.get({
89649
+ path: { message_id: mid },
89650
+ });
89651
+ const items = (_b = (_a = r.data) === null || _a === void 0 ? void 0 : _a.items) !== null && _b !== void 0 ? _b : [];
89652
+ return items;
89653
+ }
89654
+ catch (e) {
89655
+ (_d = (_c = this.logger).warn) === null || _d === void 0 ? void 0 : _d.call(_c, 'channel: fetchSubMessages failed', e);
89656
+ return [];
89657
+ }
89658
+ });
89659
+ const normalizeOpts = {
89660
+ botIdentity: this.botIdentity,
89661
+ stripBotMentions: true,
89662
+ includeRaw: this.opts.includeRawInMessage,
89663
+ fetchSubMessages,
89664
+ };
89665
+ this.dispatcher.register({
89666
+ // IM message — full safety pipeline
89667
+ 'im.message.receive_v1': (raw) => __awaiter(this, void 0, void 0, function* () {
89668
+ try {
89669
+ const msg = yield normalize(raw, normalizeOpts);
89670
+ yield this.safety.pushMessage(msg);
89671
+ }
89672
+ catch (e) {
89673
+ this.emitError(e);
89674
+ }
89675
+ }),
89676
+ // Card button click — dedup + lock + queue (by chatId).
89677
+ // The key includes the action's identity (tag + value) so that
89678
+ // different buttons on the same card by the same user are NOT
89679
+ // collapsed by the dedup cache. A genuine Feishu re-delivery
89680
+ // of the same click still hashes to the same key.
89681
+ 'card.action.trigger': (raw) => __awaiter(this, void 0, void 0, function* () {
89682
+ const evt = normalizeCardAction(raw);
89683
+ if (!evt)
89684
+ return;
89685
+ const actionId = cardActionId(evt.action);
89686
+ yield this.safety.pushAction(`card:${evt.messageId}:${evt.operator.openId}:${actionId}`, evt.chatId, () => __awaiter(this, void 0, void 0, function* () {
89687
+ const h = this.handlers.cardAction;
89688
+ if (h)
89689
+ yield h(evt);
89690
+ }));
89691
+ }),
89692
+ // Reactions — dedup only
89693
+ 'im.message.reaction.created_v1': (raw) => __awaiter(this, void 0, void 0, function* () {
89694
+ const evt = normalizeReaction(raw, 'added');
89695
+ if (!evt)
89696
+ return;
89697
+ const key = reactionKey(evt);
89698
+ yield this.safety.pushLight(key, () => { var _a, _b; return (_b = (_a = this.handlers).reaction) === null || _b === void 0 ? void 0 : _b.call(_a, evt); });
89699
+ }),
89700
+ 'im.message.reaction.deleted_v1': (raw) => __awaiter(this, void 0, void 0, function* () {
89701
+ const evt = normalizeReaction(raw, 'removed');
89702
+ if (!evt)
89703
+ return;
89704
+ const key = reactionKey(evt);
89705
+ yield this.safety.pushLight(key, () => { var _a, _b; return (_b = (_a = this.handlers).reaction) === null || _b === void 0 ? void 0 : _b.call(_a, evt); });
89706
+ }),
89707
+ // Bot added — direct fire, no safety
89708
+ 'im.chat.member.bot.added_v1': (raw) => {
89709
+ var _a, _b;
89710
+ const evt = normalizeBotAdded(raw, {
89711
+ includeRaw: this.opts.includeRawInMessage,
89712
+ });
89713
+ if (!evt)
89714
+ return;
89715
+ try {
89716
+ (_b = (_a = this.handlers).botAdded) === null || _b === void 0 ? void 0 : _b.call(_a, evt);
89717
+ }
89718
+ catch (e) {
89719
+ this.emitError(e);
89720
+ }
89721
+ },
89722
+ // Drive comments — dedup + lock + queue (by fileToken)
89723
+ 'drive.notice.comment_add_v1': (raw) => __awaiter(this, void 0, void 0, function* () {
89724
+ const evt = normalizeComment(raw, {
89725
+ includeRaw: this.opts.includeRawInMessage,
89726
+ });
89727
+ if (!evt)
89728
+ return;
89729
+ yield this.safety.pushAction(`comment:${evt.fileToken}:${evt.commentId}`, evt.fileToken, () => __awaiter(this, void 0, void 0, function* () {
89730
+ const h = this.handlers.comment;
89731
+ if (h)
89732
+ yield h(evt);
89733
+ }));
89734
+ }),
89735
+ });
89736
+ }
89737
+ emitError(e) {
89738
+ var _a, _b, _c;
89739
+ const err = e instanceof LarkChannelError
89740
+ ? e
89741
+ : new LarkChannelError('unknown', String((_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e), { cause: e });
89742
+ const handler = this.handlers.error;
89743
+ if (handler)
89744
+ handler(err);
89745
+ else
89746
+ (_c = (_b = this.logger).error) === null || _c === void 0 ? void 0 : _c.call(_b, 'channel: unhandled error', err);
89747
+ }
89748
+ }
89749
+ function createLarkChannel(opts) {
89750
+ return new LarkChannel(opts);
89751
+ }
89752
+ function bufferFromStream(raw) {
89753
+ return __awaiter(this, void 0, void 0, function* () {
89754
+ if (Buffer.isBuffer(raw))
89755
+ return raw;
89756
+ if (raw instanceof Uint8Array)
89757
+ return Buffer.from(raw);
89758
+ if (typeof raw === 'object' && raw !== null) {
89759
+ const r = raw;
89760
+ // The code-gen download endpoints (im.v1.image.get / im.v1.file.get)
89761
+ // return a wrapper object `{ writeFile, getReadableStream, headers }`
89762
+ // where the body is exposed as a stream. Consume it into a Buffer.
89763
+ if (typeof r.getReadableStream === 'function') {
89764
+ return yield readableToBuffer(r.getReadableStream());
89765
+ }
89766
+ if (Buffer.isBuffer(r.data))
89767
+ return r.data;
89768
+ if (r.data instanceof Uint8Array)
89769
+ return Buffer.from(r.data);
89770
+ }
89771
+ throw new LarkChannelError('unknown', 'unexpected download response type');
89772
+ });
89773
+ }
89774
+ function readableToBuffer(stream) {
89775
+ return new Promise((resolve, reject) => {
89776
+ const chunks = [];
89777
+ stream.on('data', (chunk) => {
89778
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
89779
+ });
89780
+ stream.on('end', () => resolve(Buffer.concat(chunks)));
89781
+ stream.on('error', reject);
89782
+ });
89783
+ }
89784
+ function reactionKey(evt) {
89785
+ var _a;
89786
+ return `rx:${evt.messageId}:${evt.operator.openId}:${evt.emojiType}:${evt.action}:${(_a = evt.actionTime) !== null && _a !== void 0 ? _a : 0}`;
89787
+ }
89788
+ /**
89789
+ * Build a stable identity for a card action event's button/element, so that
89790
+ * different clicks on the same card by the same user dedup independently.
89791
+ * `tag` plus serialized `value` is enough to tell buttons apart; `name` and
89792
+ * `option` are rolled in for form-style interactions where the same value
89793
+ * may repeat but the triggering element differs. The serialized payload is
89794
+ * length-clamped to keep cache keys small.
89795
+ */
89796
+ function cardActionId(action) {
89797
+ var _a, _b, _c;
89798
+ const serialized = typeof action.value === 'string' ? action.value : JSON.stringify((_a = action.value) !== null && _a !== void 0 ? _a : '');
89799
+ const valuePart = serialized.length > 128 ? serialized.slice(0, 128) : serialized;
89800
+ return `${action.tag}|${(_b = action.name) !== null && _b !== void 0 ? _b : ''}|${(_c = action.option) !== null && _c !== void 0 ? _c : ''}|${valuePart}`;
89801
+ }
89802
+
89803
+ export { AESCipher, Aily, AppType, CAppTicket, CTenantAccessToken, CardActionHandler, Client, Domain, EventDispatcher, LarkChannel, LarkChannelError, LoggerLevel, WSClient, adaptDefault, adaptExpress, adaptKoa, adaptKoaRouter, createLarkChannel, defaultHttpInstance, generateChallenge, messageCard, normalize, normalizeBotAdded, normalizeCardAction, normalizeComment, normalizeReaction, registerApp, withAll, withHelpDeskCredential, withTenantKey, withTenantToken, withUserAccessToken };