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/README.md +6 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +531 -87
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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: "
|
|
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 (
|
|
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
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|