eve-lark 0.2.7 → 0.3.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/dist/index.js CHANGED
@@ -563,6 +563,45 @@ function buildErrorCard(message) {
563
563
  };
564
564
  }
565
565
  __name(buildErrorCard, "buildErrorCard");
566
+ var ASK_BUTTON_VALUE_MARKER = "__eveLarkAsk";
567
+ function buildAskCard(request) {
568
+ const elements = [
569
+ { tag: "div", text: { tag: "lark_md", content: request.prompt } }
570
+ ];
571
+ if (request.options && request.options.length > 0) {
572
+ const buttons = request.options.map((opt) => ({
573
+ tag: "button",
574
+ text: { tag: "plain_text", content: opt.label },
575
+ type: opt.style ?? "default",
576
+ value: {
577
+ [ASK_BUTTON_VALUE_MARKER]: true,
578
+ requestId: request.requestId,
579
+ optionId: opt.id
580
+ },
581
+ ...opt.description ? { confirm: { title: { tag: "plain_text", content: opt.label }, text: { tag: "plain_text", content: opt.description } } } : {}
582
+ }));
583
+ elements.push({ tag: "action", actions: buttons });
584
+ }
585
+ if (request.allowFreeform) {
586
+ const hint = request.options && request.options.length > 0 ? "_\u2026or reply to this chat with your own answer_" : "_Reply to this chat with your answer_";
587
+ elements.push({ tag: "div", text: { tag: "lark_md", content: hint } });
588
+ }
589
+ return { config: { ...BASE_CONFIG }, elements };
590
+ }
591
+ __name(buildAskCard, "buildAskCard");
592
+ function buildAskAnsweredCard(request, selected) {
593
+ const elements = [
594
+ { tag: "div", text: { tag: "lark_md", content: request.prompt } }
595
+ ];
596
+ const summary = selected.kind === "option" ? `<font color='green'>\u2713 ${escapeMarkdown(selected.label)}</font>` : `<font color='green'>\u2713 ${escapeMarkdown(selected.text)}</font>`;
597
+ elements.push({ tag: "div", text: { tag: "lark_md", content: summary } });
598
+ return { config: { ...BASE_CONFIG }, elements };
599
+ }
600
+ __name(buildAskAnsweredCard, "buildAskAnsweredCard");
601
+ function escapeMarkdown(s) {
602
+ return s.replace(/[*_`~\[\]]/g, (m) => `\\${m}`);
603
+ }
604
+ __name(escapeMarkdown, "escapeMarkdown");
566
605
 
567
606
  // src/streaming-controller.ts
568
607
  var StreamingCardController = class {
@@ -768,7 +807,7 @@ var DEFAULTS = {
768
807
  maxRetries: 2,
769
808
  tokenRefreshBufferMs: 5 * 60 * 1e3,
770
809
  signatureSkewMs: 5 * 60 * 1e3,
771
- ackReaction: "TYPING",
810
+ ackReaction: "Typing",
772
811
  mode: "long-connection"
773
812
  };
774
813
  var ENV_KEYS = {
@@ -979,7 +1018,25 @@ async function doStartLongConnection(args) {
979
1018
  } catch (e) {
980
1019
  logError(`forward failed (event dropped)`, e);
981
1020
  }
982
- }, "im.message.receive_v1")
1021
+ }, "im.message.receive_v1"),
1022
+ // Card-button clicks. Feishu's card.action.trigger fires when a user
1023
+ // taps a button on a card we rendered. Forward to the channel webhook
1024
+ // — the webhook handler dispatches by event_type and feeds the click
1025
+ // back into eve as an InputResponse.
1026
+ "card.action.trigger": /* @__PURE__ */ __name(async (data) => {
1027
+ try {
1028
+ const envelope = rebuildEnvelopeFromSdkEvent("card.action.trigger", data, {
1029
+ appId: args.resolved.appId,
1030
+ verificationToken: args.resolved.verificationToken
1031
+ });
1032
+ await postEventToWebhookRetry(envelope, {
1033
+ eveWebhookUrl: args.eveWebhookUrl,
1034
+ encryptKey: args.resolved.encryptKey
1035
+ });
1036
+ } catch (e) {
1037
+ logError(`card action forward failed (event dropped)`, e);
1038
+ }
1039
+ }, "card.action.trigger")
983
1040
  });
984
1041
  const domain = args.resolved.baseUrl.includes("larksuite.com") ? sdk.Domain.Lark : sdk.Domain.Feishu;
985
1042
  const wsClient = new sdk.WSClient({
@@ -1006,6 +1063,196 @@ async function loadLarkSdk() {
1006
1063
  }
1007
1064
  __name(loadLarkSdk, "loadLarkSdk");
1008
1065
 
1066
+ // src/feishu-emoji.ts
1067
+ var VALID_FEISHU_EMOJI_TYPES = /* @__PURE__ */ new Set([
1068
+ "OK",
1069
+ "THUMBSUP",
1070
+ "THANKS",
1071
+ "MUSCLE",
1072
+ "FINGERHEART",
1073
+ "APPLAUSE",
1074
+ "FISTBUMP",
1075
+ "JIAYI",
1076
+ "DONE",
1077
+ "SMILE",
1078
+ "BLUSH",
1079
+ "LAUGH",
1080
+ "SMIRK",
1081
+ "LOL",
1082
+ "FACEPALM",
1083
+ "LOVE",
1084
+ "WINK",
1085
+ "PROUD",
1086
+ "WITTY",
1087
+ "SMART",
1088
+ "SCOWL",
1089
+ "THINKING",
1090
+ "SOB",
1091
+ "CRY",
1092
+ "ERROR",
1093
+ "NOSEPICK",
1094
+ "HAUGHTY",
1095
+ "SLAP",
1096
+ "SPITBLOOD",
1097
+ "TOASTED",
1098
+ "GLANCE",
1099
+ "DULL",
1100
+ "INNOCENTSMILE",
1101
+ "JOYFUL",
1102
+ "WOW",
1103
+ "TRICK",
1104
+ "YEAH",
1105
+ "ENOUGH",
1106
+ "TEARS",
1107
+ "EMBARRASSED",
1108
+ "KISS",
1109
+ "SMOOCH",
1110
+ "DROOL",
1111
+ "OBSESSED",
1112
+ "MONEY",
1113
+ "TEASE",
1114
+ "SHOWOFF",
1115
+ "COMFORT",
1116
+ "CLAP",
1117
+ "PRAISE",
1118
+ "STRIVE",
1119
+ "XBLUSH",
1120
+ "SILENT",
1121
+ "WAVE",
1122
+ "WHAT",
1123
+ "FROWN",
1124
+ "SHY",
1125
+ "DIZZY",
1126
+ "LOOKDOWN",
1127
+ "CHUCKLE",
1128
+ "WAIL",
1129
+ "CRAZY",
1130
+ "WHIMPER",
1131
+ "HUG",
1132
+ "BLUBBER",
1133
+ "WRONGED",
1134
+ "HUSKY",
1135
+ "SHHH",
1136
+ "SMUG",
1137
+ "ANGRY",
1138
+ "HAMMER",
1139
+ "SHOCKED",
1140
+ "TERROR",
1141
+ "PETRIFIED",
1142
+ "SKULL",
1143
+ "SWEAT",
1144
+ "SPEECHLESS",
1145
+ "SLEEP",
1146
+ "DROWSY",
1147
+ "YAWN",
1148
+ "SICK",
1149
+ "PUKE",
1150
+ "BETRAYED",
1151
+ "HEADSET",
1152
+ "EatingFood",
1153
+ "MeMeMe",
1154
+ "Sigh",
1155
+ "Typing",
1156
+ "SLIGHT",
1157
+ "TONGUE",
1158
+ "EYESCLOSED",
1159
+ "RoarForYou",
1160
+ "CALF",
1161
+ "BEAR",
1162
+ "BULL",
1163
+ "RAINBOWPUKE",
1164
+ "Lemon",
1165
+ "ROSE",
1166
+ "HEART",
1167
+ "PARTY",
1168
+ "LIPS",
1169
+ "BEER",
1170
+ "CAKE",
1171
+ "GIFT",
1172
+ "CUCUMBER",
1173
+ "Drumstick",
1174
+ "Pepper",
1175
+ "CANDIEDHAWS",
1176
+ "BubbleTea",
1177
+ "Coffee",
1178
+ "Get",
1179
+ "LGTM",
1180
+ "OnIt",
1181
+ "OneSecond",
1182
+ "VRHeadset",
1183
+ "YouAreTheBest",
1184
+ "SALUTE",
1185
+ "SHAKE",
1186
+ "HIGHFIVE",
1187
+ "UPPERLEFT",
1188
+ "ThumbsDown",
1189
+ "Yes",
1190
+ "No",
1191
+ "OKR",
1192
+ "CheckMark",
1193
+ "CrossMark",
1194
+ "MinusOne",
1195
+ "Hundred",
1196
+ "AWESOMEN",
1197
+ "Pin",
1198
+ "Alarm",
1199
+ "Loudspeaker",
1200
+ "Trophy",
1201
+ "Fire",
1202
+ "BOMB",
1203
+ "Music",
1204
+ "XmasTree",
1205
+ "Snowman",
1206
+ "XmasHat",
1207
+ "FIREWORKS",
1208
+ "2022",
1209
+ "REDPACKET",
1210
+ "FORTUNE",
1211
+ "LUCK",
1212
+ "FIRECRACKER",
1213
+ "StickyRiceBalls",
1214
+ "HEARTBROKEN",
1215
+ "POOP",
1216
+ "StatusFlashOfInspiration",
1217
+ "18X",
1218
+ "CLEAVER",
1219
+ "Soccer",
1220
+ "Basketball",
1221
+ "GeneralDoNotDisturb",
1222
+ "Status_PrivateMessage",
1223
+ "GeneralInMeetingBusy",
1224
+ "StatusReading",
1225
+ "StatusInFlight",
1226
+ "GeneralBusinessTrip",
1227
+ "GeneralWorkFromHome",
1228
+ "StatusEnjoyLife",
1229
+ "GeneralTravellingCar",
1230
+ "StatusBus",
1231
+ "GeneralSun",
1232
+ "GeneralMoonRest",
1233
+ "MoonRabbit",
1234
+ "Mooncake",
1235
+ "JubilantRabbit",
1236
+ "TV",
1237
+ "Movie",
1238
+ "Pumpkin",
1239
+ "BeamingFace",
1240
+ "Delighted",
1241
+ "ColdSweat",
1242
+ "FullMoonFace",
1243
+ "Partying",
1244
+ "GoGoGo",
1245
+ "ThanksFace",
1246
+ "SaluteFace",
1247
+ "Shrug",
1248
+ "ClownFace",
1249
+ "HappyDragon"
1250
+ ]);
1251
+ function isValidFeishuEmojiType(s) {
1252
+ return VALID_FEISHU_EMOJI_TYPES.has(s);
1253
+ }
1254
+ __name(isValidFeishuEmojiType, "isValidFeishuEmojiType");
1255
+
1009
1256
  // src/channel.ts
1010
1257
  var MAX_BODY_BYTES = 1e6;
1011
1258
  var STALE_SESSION_MS = 30 * 60 * 1e3;
@@ -1031,11 +1278,33 @@ function ackOk() {
1031
1278
  }
1032
1279
  __name(ackOk, "ackOk");
1033
1280
  function pickAckEmoji(reaction) {
1034
- if (typeof reaction === "string") return reaction;
1281
+ if (reaction === false) return false;
1282
+ if (typeof reaction === "string") {
1283
+ if (!isValidFeishuEmojiType(reaction)) {
1284
+ console.warn(
1285
+ `[eve-lark] ackReaction "${reaction}" is not a valid Feishu emoji type (case-sensitive; e.g. "Typing" not "TYPING"). Skipping ack reaction. See VALID_FEISHU_EMOJI_TYPES for the full list.`
1286
+ );
1287
+ return false;
1288
+ }
1289
+ return reaction;
1290
+ }
1035
1291
  if (Array.isArray(reaction)) {
1036
- if (reaction.length === 0) return false;
1037
- const idx = Math.floor(Math.random() * reaction.length);
1038
- return reaction[idx] ?? false;
1292
+ const valid = reaction.filter(isValidFeishuEmojiType);
1293
+ if (valid.length === 0) {
1294
+ const sample = reaction.slice(0, 3).join(", ");
1295
+ console.warn(
1296
+ `[eve-lark] ackReaction array contains no valid Feishu emoji types (got [${sample}${reaction.length > 3 ? ", \u2026" : ""}]). Skipping ack reaction.`
1297
+ );
1298
+ return false;
1299
+ }
1300
+ if (valid.length < reaction.length) {
1301
+ const dropped = reaction.filter((e) => !isValidFeishuEmojiType(e));
1302
+ console.warn(
1303
+ `[eve-lark] ackReaction array dropped ${dropped.length} invalid emoji type(s): ${dropped.slice(0, 3).join(", ")}`
1304
+ );
1305
+ }
1306
+ const idx = Math.floor(Math.random() * valid.length);
1307
+ return valid[idx] ?? false;
1039
1308
  }
1040
1309
  return false;
1041
1310
  }
@@ -1083,6 +1352,8 @@ function createLarkChannel(optionsInput) {
1083
1352
  }
1084
1353
  const controllers = /* @__PURE__ */ new Map();
1085
1354
  const sessionMeta = /* @__PURE__ */ new Map();
1355
+ const pendingInputsByRequestId = /* @__PURE__ */ new Map();
1356
+ const pendingInputsByChatToken = /* @__PURE__ */ new Map();
1086
1357
  function getController(sessionId, meta) {
1087
1358
  let ctrl = controllers.get(sessionId);
1088
1359
  if (!ctrl) {
@@ -1219,8 +1490,29 @@ function createLarkChannel(optionsInput) {
1219
1490
  sessionMeta.delete(id);
1220
1491
  }
1221
1492
  }
1493
+ for (const [reqId, p] of pendingInputsByRequestId) {
1494
+ if (p.touchedAt < cutoff) {
1495
+ pendingInputsByRequestId.delete(reqId);
1496
+ const tokenKey = chatTokenKey(p.chatId, p.rootId, p.parentId);
1497
+ if (pendingInputsByChatToken.get(tokenKey)?.requestId === reqId) {
1498
+ pendingInputsByChatToken.delete(tokenKey);
1499
+ }
1500
+ }
1501
+ }
1222
1502
  }
1223
1503
  __name(maybeSweep, "maybeSweep");
1504
+ function chatTokenKey(chatId, rootId, parentId) {
1505
+ return `${chatId}:${parentId ?? rootId ?? "_"}`;
1506
+ }
1507
+ __name(chatTokenKey, "chatTokenKey");
1508
+ function dropPendingInput(p) {
1509
+ pendingInputsByRequestId.delete(p.requestId);
1510
+ const tokenKey = chatTokenKey(p.chatId, p.rootId, p.parentId);
1511
+ if (pendingInputsByChatToken.get(tokenKey)?.requestId === p.requestId) {
1512
+ pendingInputsByChatToken.delete(tokenKey);
1513
+ }
1514
+ }
1515
+ __name(dropPendingInput, "dropPendingInput");
1224
1516
  const webhookHandler = /* @__PURE__ */ __name(async (req, helpers) => {
1225
1517
  maybeSweep();
1226
1518
  const contentLength = Number(req.headers.get("content-length") ?? "0");
@@ -1273,12 +1565,17 @@ function createLarkChannel(optionsInput) {
1273
1565
  if (body.header?.token !== options.verificationToken) {
1274
1566
  return new Response("verification token mismatch", { status: 401 });
1275
1567
  }
1276
- const dedupKey = body.header?.event_id ?? body.event?.message?.message_id;
1568
+ const evtMsg = body.event;
1569
+ const dedupKey = body.header?.event_id ?? evtMsg?.message?.message_id ?? evtMsg?.open_message_id;
1277
1570
  if (dedupKey) {
1278
1571
  if (dedup.has(dedupKey)) return ackOk();
1279
1572
  dedup.set(dedupKey);
1280
1573
  }
1281
- if (body.header?.event_type !== "im.message.receive_v1") {
1574
+ const eventType = body.header?.event_type;
1575
+ if (eventType === "card.action.trigger") {
1576
+ return handleCardAction(body.event, helpers);
1577
+ }
1578
+ if (eventType !== "im.message.receive_v1") {
1282
1579
  return ackOk();
1283
1580
  }
1284
1581
  if (!body.event) return ackOk();
@@ -1289,6 +1586,44 @@ function createLarkChannel(optionsInput) {
1289
1586
  if (parsed.text === "" && parsed.files.length === 0) {
1290
1587
  return ackOk();
1291
1588
  }
1589
+ const tokenKey = chatTokenKey(parsed.chatId, parsed.rootId ?? void 0, parsed.parentId ?? void 0);
1590
+ const pending = pendingInputsByChatToken.get(tokenKey);
1591
+ if (pending && pending.awaitingFreeform && parsed.text.length > 0) {
1592
+ const resp = { requestId: pending.requestId, text: parsed.text };
1593
+ const resumeAuth = {
1594
+ authenticator: "lark",
1595
+ principalType: "user",
1596
+ principalId: parsed.senderOpenId,
1597
+ attributes: {
1598
+ chatId: parsed.chatId,
1599
+ rootMessageId: parsed.rootId,
1600
+ messageId: parsed.messageId,
1601
+ chatType: parsed.chatType
1602
+ }
1603
+ };
1604
+ const resumeToken = larkContinuationToken(parsed.chatId, parsed.parentId ?? parsed.rootId);
1605
+ try {
1606
+ await helpers.send(
1607
+ { inputResponses: [resp] },
1608
+ { auth: resumeAuth, continuationToken: resumeToken }
1609
+ );
1610
+ if (pending.cardMessageId) {
1611
+ try {
1612
+ await client.patchCard({
1613
+ messageId: pending.cardMessageId,
1614
+ card: buildAskAnsweredCard(pending.request, { kind: "freeform", text: parsed.text })
1615
+ });
1616
+ } catch (e) {
1617
+ console.warn("[eve-lark] patchCard after freeform answer failed:", e instanceof Error ? e.message : e);
1618
+ }
1619
+ }
1620
+ } catch (e) {
1621
+ console.error("[eve-lark] freeform input-response send failed:", e instanceof Error ? e.message : e);
1622
+ } finally {
1623
+ dropPendingInput(pending);
1624
+ }
1625
+ return ackOk();
1626
+ }
1292
1627
  const userContent = buildUserContent(parsed.text, parsed.files, options, parsed.messageId);
1293
1628
  const continuationToken = larkContinuationToken(parsed.chatId, parsed.parentId ?? parsed.rootId);
1294
1629
  const auth = {
@@ -1330,7 +1665,191 @@ function createLarkChannel(optionsInput) {
1330
1665
  }
1331
1666
  return ackOk();
1332
1667
  }, "webhookHandler");
1333
- return defineChannel({
1668
+ async function handleCardAction(evt, helpers) {
1669
+ const value = evt.action?.value;
1670
+ if (!value || value[ASK_BUTTON_VALUE_MARKER] !== true) {
1671
+ return ackOk();
1672
+ }
1673
+ const requestId = typeof value.requestId === "string" ? value.requestId : "";
1674
+ const optionId = typeof value.optionId === "string" ? value.optionId : "";
1675
+ if (!requestId) return ackOk();
1676
+ const pending = pendingInputsByRequestId.get(requestId);
1677
+ if (!pending) {
1678
+ console.warn(`[eve-lark] card action for unknown requestId=${requestId} (already answered or expired)`);
1679
+ return ackOk();
1680
+ }
1681
+ const resp = { requestId, optionId: optionId || void 0 };
1682
+ const resumeToken = larkContinuationToken(pending.chatId, pending.parentId ?? pending.rootId ?? null);
1683
+ const resumeAuth = {
1684
+ authenticator: "lark",
1685
+ principalType: "user",
1686
+ principalId: evt.open_id,
1687
+ attributes: {
1688
+ chatId: pending.chatId,
1689
+ rootMessageId: pending.rootId,
1690
+ messageId: evt.open_message_id,
1691
+ chatType: pending.request.display === "confirmation" ? "p2p" : "group"
1692
+ }
1693
+ };
1694
+ try {
1695
+ await helpers.send(
1696
+ { inputResponses: [resp] },
1697
+ { auth: resumeAuth, continuationToken: resumeToken }
1698
+ );
1699
+ console.log(`[eve-lark] ask answered via button click requestId=${requestId} optionId=${optionId}`);
1700
+ } catch (e) {
1701
+ console.error(
1702
+ `[eve-lark] ask input-response send failed (requestId=${requestId}):`,
1703
+ e instanceof Error ? e.message : e
1704
+ );
1705
+ }
1706
+ const selectedOpt = pending.request.options?.find((o) => o.id === optionId);
1707
+ if (pending.cardMessageId && selectedOpt) {
1708
+ try {
1709
+ await client.patchCard({
1710
+ messageId: pending.cardMessageId,
1711
+ card: buildAskAnsweredCard(pending.request, { kind: "option", label: selectedOpt.label })
1712
+ });
1713
+ } catch (e) {
1714
+ console.warn("[eve-lark] patchCard after ask-answer failed:", e instanceof Error ? e.message : e);
1715
+ }
1716
+ }
1717
+ dropPendingInput(pending);
1718
+ return ackOk();
1719
+ }
1720
+ __name(handleCardAction, "handleCardAction");
1721
+ const channelEvents = {
1722
+ // Streaming delta — patch the card.
1723
+ "message.appended"(data, _channel, ctx) {
1724
+ if (options.replyMode !== "streaming") return;
1725
+ const sessionId = ctx.session.id;
1726
+ const info = sessionInfoFromCtx(ctx);
1727
+ if (!info) return;
1728
+ const d = data;
1729
+ if (typeof d.messageDelta !== "string") return;
1730
+ const ctrl = getController(sessionId, info);
1731
+ ctrl.appendDelta(d.messageDelta);
1732
+ },
1733
+ // eve's ask_question (and similar HITL tools) fire this event with a
1734
+ // list of input requests. Each request becomes a Feishu card with
1735
+ // buttons (one per option) plus optional freeform hint.
1736
+ async "input.requested"(data, _channel, ctx) {
1737
+ const sessionId = ctx.session.id;
1738
+ const info = sessionInfoFromCtx(ctx);
1739
+ if (!info) {
1740
+ console.warn(`[eve-lark] input.requested: no session info (sessionId=${sessionId})`);
1741
+ return;
1742
+ }
1743
+ const d = data;
1744
+ const requests = d.requests ?? [];
1745
+ if (requests.length === 0) return;
1746
+ console.log(
1747
+ `[eve-lark] input.requested sessionId=${sessionId} chatId=${info.chatId} count=${requests.length}`
1748
+ );
1749
+ for (const req of requests) {
1750
+ const card = buildAskCard(req);
1751
+ let cardMessageId;
1752
+ try {
1753
+ const res = await client.sendCard({
1754
+ chatId: info.chatId,
1755
+ card,
1756
+ rootId: info.rootId,
1757
+ parentId: info.parentId
1758
+ });
1759
+ cardMessageId = res.messageId;
1760
+ } catch (e) {
1761
+ console.error(
1762
+ `[eve-lark] ask card send failed (requestId=${req.requestId}):`,
1763
+ e instanceof Error ? e.message : e
1764
+ );
1765
+ continue;
1766
+ }
1767
+ const pending = {
1768
+ requestId: req.requestId,
1769
+ sessionId,
1770
+ chatId: info.chatId,
1771
+ rootId: info.rootId,
1772
+ parentId: info.parentId,
1773
+ cardMessageId,
1774
+ request: req,
1775
+ createdAt: Date.now(),
1776
+ touchedAt: Date.now(),
1777
+ awaitingFreeform: req.allowFreeform === true
1778
+ };
1779
+ pendingInputsByRequestId.set(req.requestId, pending);
1780
+ if (pending.awaitingFreeform) {
1781
+ const tokenKey = chatTokenKey(info.chatId, info.rootId, info.parentId);
1782
+ pendingInputsByChatToken.set(tokenKey, pending);
1783
+ }
1784
+ }
1785
+ },
1786
+ // Terminal — deliver the final reply, then clean up the ack reaction.
1787
+ async "message.completed"(data, _channel, ctx) {
1788
+ const sessionId = ctx.session.id;
1789
+ const info = sessionInfoFromCtx(ctx);
1790
+ if (!info) {
1791
+ console.warn(`[eve-lark] message.completed: no session info, cannot deliver (sessionId=${sessionId})`);
1792
+ return;
1793
+ }
1794
+ const d = data;
1795
+ const rawText = typeof d.message === "string" ? d.message : "";
1796
+ console.log(
1797
+ `[eve-lark] message.completed sessionId=${sessionId} chatId=${info.chatId} msgLen=${rawText.length}`
1798
+ );
1799
+ const text = rawText.length > 0 ? rawText : EMPTY_REPLY_TEXT;
1800
+ try {
1801
+ await deliverReply(sessionId, info, text);
1802
+ } finally {
1803
+ await cleanupAckReaction(sessionId);
1804
+ dropController(sessionId);
1805
+ }
1806
+ },
1807
+ async "turn.failed"(data, _channel, ctx) {
1808
+ const sessionId = ctx?.session?.id;
1809
+ if (!sessionId) {
1810
+ console.warn("[eve-lark] turn.failed: no sessionId on ctx");
1811
+ return;
1812
+ }
1813
+ const info = sessionInfoFromCtx(ctx);
1814
+ if (!info) {
1815
+ console.warn(`[eve-lark] turn.failed: no session info (sessionId=${sessionId})`);
1816
+ return;
1817
+ }
1818
+ const errMsg = errMsgFrom(data, "turn failed");
1819
+ console.warn(
1820
+ `[eve-lark] turn.failed sessionId=${sessionId} chatId=${info.chatId} err="${errMsg.slice(0, 200)}"`
1821
+ );
1822
+ const userText = `\u26A0 ${errMsg}`;
1823
+ const ctrl = controllers.get(sessionId);
1824
+ if (ctrl) {
1825
+ try {
1826
+ await ctrl.abort(errMsg);
1827
+ console.log(`[eve-lark] error shown via streaming abort (sessionId=${sessionId})`);
1828
+ } catch (e) {
1829
+ console.warn(
1830
+ `[eve-lark] turn.failed: streaming abort failed, will deliver fresh error (sessionId=${sessionId}):`,
1831
+ e instanceof Error ? e.message : e
1832
+ );
1833
+ try {
1834
+ await deliverReply(sessionId, info, userText);
1835
+ } catch {
1836
+ }
1837
+ }
1838
+ } else {
1839
+ try {
1840
+ await deliverReply(sessionId, info, userText);
1841
+ } catch {
1842
+ }
1843
+ }
1844
+ await cleanupAckReaction(sessionId);
1845
+ dropController(sessionId);
1846
+ },
1847
+ async "session.failed"(data) {
1848
+ const errMsg = errMsgFrom(data, "session failed");
1849
+ console.error("[eve-lark] session.failed:", errMsg);
1850
+ }
1851
+ };
1852
+ const channel = defineChannel({
1334
1853
  routes: [POST(options.webhookPath, webhookHandler)],
1335
1854
  fetchFile: /* @__PURE__ */ __name(async (url) => {
1336
1855
  if (!url.startsWith(options.baseUrl)) return null;
@@ -1342,85 +1861,10 @@ function createLarkChannel(optionsInput) {
1342
1861
  type: m[3]
1343
1862
  });
1344
1863
  }, "fetchFile"),
1345
- events: {
1346
- // Streaming delta — patch the card.
1347
- "message.appended"(data, _channel, ctx) {
1348
- if (options.replyMode !== "streaming") return;
1349
- const sessionId = ctx.session.id;
1350
- const info = sessionInfoFromCtx(ctx);
1351
- if (!info) return;
1352
- const d = data;
1353
- if (typeof d.messageDelta !== "string") return;
1354
- const ctrl = getController(sessionId, info);
1355
- ctrl.appendDelta(d.messageDelta);
1356
- },
1357
- // Terminal — deliver the final reply, then clean up the ack reaction.
1358
- async "message.completed"(data, _channel, ctx) {
1359
- const sessionId = ctx.session.id;
1360
- const info = sessionInfoFromCtx(ctx);
1361
- if (!info) {
1362
- console.warn(`[eve-lark] message.completed: no session info, cannot deliver (sessionId=${sessionId})`);
1363
- return;
1364
- }
1365
- const d = data;
1366
- const rawText = typeof d.message === "string" ? d.message : "";
1367
- console.log(
1368
- `[eve-lark] message.completed sessionId=${sessionId} chatId=${info.chatId} msgLen=${rawText.length}`
1369
- );
1370
- const text = rawText.length > 0 ? rawText : EMPTY_REPLY_TEXT;
1371
- try {
1372
- await deliverReply(sessionId, info, text);
1373
- } finally {
1374
- await cleanupAckReaction(sessionId);
1375
- dropController(sessionId);
1376
- }
1377
- },
1378
- async "turn.failed"(data, _channel, ctx) {
1379
- const sessionId = ctx?.session?.id;
1380
- if (!sessionId) {
1381
- console.warn("[eve-lark] turn.failed: no sessionId on ctx");
1382
- return;
1383
- }
1384
- const info = sessionInfoFromCtx(ctx);
1385
- if (!info) {
1386
- console.warn(`[eve-lark] turn.failed: no session info (sessionId=${sessionId})`);
1387
- return;
1388
- }
1389
- const errMsg = errMsgFrom(data, "turn failed");
1390
- console.warn(
1391
- `[eve-lark] turn.failed sessionId=${sessionId} chatId=${info.chatId} err="${errMsg.slice(0, 200)}"`
1392
- );
1393
- const userText = `\u26A0 ${errMsg}`;
1394
- const ctrl = controllers.get(sessionId);
1395
- if (ctrl) {
1396
- try {
1397
- await ctrl.abort(errMsg);
1398
- console.log(`[eve-lark] error shown via streaming abort (sessionId=${sessionId})`);
1399
- } catch (e) {
1400
- console.warn(
1401
- `[eve-lark] turn.failed: streaming abort failed, will deliver fresh error (sessionId=${sessionId}):`,
1402
- e instanceof Error ? e.message : e
1403
- );
1404
- try {
1405
- await deliverReply(sessionId, info, userText);
1406
- } catch {
1407
- }
1408
- }
1409
- } else {
1410
- try {
1411
- await deliverReply(sessionId, info, userText);
1412
- } catch {
1413
- }
1414
- }
1415
- await cleanupAckReaction(sessionId);
1416
- dropController(sessionId);
1417
- },
1418
- async "session.failed"(data) {
1419
- const errMsg = errMsgFrom(data, "session failed");
1420
- console.error("[eve-lark] session.failed:", errMsg);
1421
- }
1422
- }
1864
+ events: channelEvents
1423
1865
  });
1866
+ channel.__testEvents = channelEvents;
1867
+ return channel;
1424
1868
  }
1425
1869
  __name(createLarkChannel, "createLarkChannel");
1426
1870
  export {