eve-lark 0.3.1 → 0.4.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.d.ts +70 -1
- package/dist/index.js +321 -55
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -64,6 +64,44 @@ interface LarkChannelOptions {
|
|
|
64
64
|
* Defaults to `$PORT` or `2000` (matches `eve dev`).
|
|
65
65
|
*/
|
|
66
66
|
port?: number | undefined;
|
|
67
|
+
/**
|
|
68
|
+
* Allowlist of sender open_ids for DM (p2p) messages. When set, DMs from
|
|
69
|
+
* senders not in this list are dropped before reaching the agent. Has no
|
|
70
|
+
* effect on group messages (use `groupAllowFrom` for those). Default:
|
|
71
|
+
* unset → all DMs allowed.
|
|
72
|
+
*/
|
|
73
|
+
allowFrom?: readonly string[] | undefined;
|
|
74
|
+
/**
|
|
75
|
+
* Allowlist of chat_ids for group messages. When set, messages from
|
|
76
|
+
* chats not in this list are dropped. Default: unset → all groups allowed.
|
|
77
|
+
*/
|
|
78
|
+
groupAllowFrom?: readonly string[] | undefined;
|
|
79
|
+
/**
|
|
80
|
+
* Per-group configuration. Matched by chat_id on inbound group messages.
|
|
81
|
+
* Currently only `systemPrompt` is read; it's injected as `context` in
|
|
82
|
+
* the `send()` call so the agent treats it as an additional user-role
|
|
83
|
+
* instruction at the start of the turn. DMs ignore this.
|
|
84
|
+
*/
|
|
85
|
+
groupConfigs?: readonly LarkGroupConfig[] | undefined;
|
|
86
|
+
/**
|
|
87
|
+
* ASR provider for audio/media transcription. When set, audio/media
|
|
88
|
+
* messages are downloaded, transcribed, and the transcript is forwarded
|
|
89
|
+
* to the agent as text. When unset (default), audio/media messages are
|
|
90
|
+
* ack-and-skipped.
|
|
91
|
+
*/
|
|
92
|
+
asrProvider?: LarkAsrProvider | undefined;
|
|
93
|
+
}
|
|
94
|
+
interface LarkGroupConfig {
|
|
95
|
+
chatId: string;
|
|
96
|
+
systemPrompt?: string | undefined;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Pluggable ASR (Automatic Speech Recognition) provider. When configured,
|
|
100
|
+
* inbound audio/media messages are downloaded, transcribed, and the transcript
|
|
101
|
+
* replaces the empty text — the agent receives it as a normal text message.
|
|
102
|
+
*/
|
|
103
|
+
interface LarkAsrProvider {
|
|
104
|
+
transcribe(audioBytes: Buffer, mediaType: string): Promise<string>;
|
|
67
105
|
}
|
|
68
106
|
interface ResolvedLarkOptions {
|
|
69
107
|
appId: string;
|
|
@@ -86,6 +124,10 @@ interface ResolvedLarkOptions {
|
|
|
86
124
|
ackReaction: string | readonly string[] | false;
|
|
87
125
|
mode: LarkTransportMode;
|
|
88
126
|
port: number;
|
|
127
|
+
allowFrom: readonly string[] | undefined;
|
|
128
|
+
groupAllowFrom: readonly string[] | undefined;
|
|
129
|
+
groupConfigs: readonly LarkGroupConfig[] | undefined;
|
|
130
|
+
asrProvider: LarkAsrProvider | undefined;
|
|
89
131
|
}
|
|
90
132
|
type LarkSenderType = "user" | "app";
|
|
91
133
|
type LarkChatType = "p2p" | "group";
|
|
@@ -197,9 +239,34 @@ type LarkCardElement = {
|
|
|
197
239
|
}>;
|
|
198
240
|
} | {
|
|
199
241
|
tag: "action";
|
|
200
|
-
actions:
|
|
242
|
+
actions: LarkCardActionItem[];
|
|
201
243
|
layout?: "bisected" | "trisection" | "flow";
|
|
202
244
|
};
|
|
245
|
+
/** Union of action-row item shapes: buttons (yes/no confirm style) and
|
|
246
|
+
* select menus (dropdowns for longer option lists). */
|
|
247
|
+
type LarkCardActionItem = LarkCardButton | LarkCardSelectMenu;
|
|
248
|
+
interface LarkCardSelectMenu {
|
|
249
|
+
tag: "select_static";
|
|
250
|
+
placeholder?: {
|
|
251
|
+
tag: "plain_text";
|
|
252
|
+
content: string;
|
|
253
|
+
};
|
|
254
|
+
/** Initially-selected option id (string). */
|
|
255
|
+
initial_option?: string;
|
|
256
|
+
/** Selectable options. `value` carries the optionId we get back in
|
|
257
|
+
* `action.option` when the user picks one. */
|
|
258
|
+
options: Array<{
|
|
259
|
+
text: {
|
|
260
|
+
tag: "plain_text";
|
|
261
|
+
content: string;
|
|
262
|
+
};
|
|
263
|
+
value: string;
|
|
264
|
+
}>;
|
|
265
|
+
/** Same marker payload as a button so the dispatcher can recognise our
|
|
266
|
+
* own callbacks. `optionId` is NOT set here — it comes back via
|
|
267
|
+
* `action.option` instead. */
|
|
268
|
+
value?: Record<string, unknown>;
|
|
269
|
+
}
|
|
203
270
|
interface LarkCardButton {
|
|
204
271
|
tag: "button";
|
|
205
272
|
text: {
|
|
@@ -207,6 +274,8 @@ interface LarkCardButton {
|
|
|
207
274
|
content: string;
|
|
208
275
|
};
|
|
209
276
|
type?: "default" | "primary" | "danger";
|
|
277
|
+
/** Opens this URL when clicked (instead of triggering card.action.trigger). */
|
|
278
|
+
url?: string;
|
|
210
279
|
/** Arbitrary JSON returned in the card.action.trigger callback's
|
|
211
280
|
* `action.value`. eve-lark sets `{ __eveLarkAsk, requestId, optionId }`. */
|
|
212
281
|
value?: Record<string, unknown>;
|
package/dist/index.js
CHANGED
|
@@ -535,6 +535,57 @@ var BASE_CONFIG = {
|
|
|
535
535
|
wide_screen_mode: true,
|
|
536
536
|
update_multi: true
|
|
537
537
|
};
|
|
538
|
+
function buildAuthCard(opts) {
|
|
539
|
+
const elements = [
|
|
540
|
+
{ tag: "div", text: { tag: "lark_md", content: `Sign in to **${escapeMarkdown(opts.displayName)}** to continue.` } },
|
|
541
|
+
{
|
|
542
|
+
tag: "action",
|
|
543
|
+
actions: [
|
|
544
|
+
{
|
|
545
|
+
tag: "button",
|
|
546
|
+
text: { tag: "plain_text", content: `Sign in with ${opts.displayName}` },
|
|
547
|
+
type: "primary",
|
|
548
|
+
url: opts.url
|
|
549
|
+
}
|
|
550
|
+
]
|
|
551
|
+
}
|
|
552
|
+
];
|
|
553
|
+
if (opts.userCode) {
|
|
554
|
+
elements.push({
|
|
555
|
+
tag: "div",
|
|
556
|
+
text: { tag: "lark_md", content: `Verification code: \`${escapeMarkdown(opts.userCode)}\`` }
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
return { config: { ...BASE_CONFIG }, elements };
|
|
560
|
+
}
|
|
561
|
+
__name(buildAuthCard, "buildAuthCard");
|
|
562
|
+
function buildAuthCompletedCard(opts) {
|
|
563
|
+
const outcomeLabel = {
|
|
564
|
+
authorized: "\u2713",
|
|
565
|
+
declined: "\u2717",
|
|
566
|
+
failed: "\u26A0",
|
|
567
|
+
"timed-out": "\u23F1"
|
|
568
|
+
};
|
|
569
|
+
const glyph = outcomeLabel[opts.outcome] ?? "\u2022";
|
|
570
|
+
const outcomeText = {
|
|
571
|
+
authorized: "connected",
|
|
572
|
+
declined: "declined",
|
|
573
|
+
failed: "failed",
|
|
574
|
+
"timed-out": "timed out"
|
|
575
|
+
};
|
|
576
|
+
const label = outcomeText[opts.outcome] ?? opts.outcome;
|
|
577
|
+
const suffix = opts.reason ? ` \u2014 ${escapeMarkdown(opts.reason)}` : "";
|
|
578
|
+
return {
|
|
579
|
+
config: { ...BASE_CONFIG },
|
|
580
|
+
elements: [
|
|
581
|
+
{
|
|
582
|
+
tag: "div",
|
|
583
|
+
text: { tag: "lark_md", content: `**${escapeMarkdown(opts.displayName)}**: ${glyph} ${label}${suffix}` }
|
|
584
|
+
}
|
|
585
|
+
]
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
__name(buildAuthCompletedCard, "buildAuthCompletedCard");
|
|
538
589
|
function buildTextCard(text) {
|
|
539
590
|
return {
|
|
540
591
|
config: { ...BASE_CONFIG },
|
|
@@ -564,26 +615,56 @@ function buildErrorCard(message) {
|
|
|
564
615
|
}
|
|
565
616
|
__name(buildErrorCard, "buildErrorCard");
|
|
566
617
|
var ASK_BUTTON_VALUE_MARKER = "__eveLarkAsk";
|
|
618
|
+
var ASK_OPTIONS_BUTTON_MAX = 3;
|
|
567
619
|
function buildAskCard(request) {
|
|
568
620
|
const elements = [
|
|
569
621
|
{ tag: "div", text: { tag: "lark_md", content: request.prompt } }
|
|
570
622
|
];
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
623
|
+
const optionCount = request.options?.length ?? 0;
|
|
624
|
+
if (optionCount > 0) {
|
|
625
|
+
const useSelect = request.display === "select" || optionCount > ASK_OPTIONS_BUTTON_MAX;
|
|
626
|
+
if (useSelect) {
|
|
627
|
+
elements.push({
|
|
628
|
+
tag: "action",
|
|
629
|
+
actions: [
|
|
630
|
+
{
|
|
631
|
+
tag: "select_static",
|
|
632
|
+
placeholder: { tag: "plain_text", content: "Select an option\u2026" },
|
|
633
|
+
options: request.options.map((opt) => ({
|
|
634
|
+
text: { tag: "plain_text", content: opt.label },
|
|
635
|
+
value: opt.id
|
|
636
|
+
})),
|
|
637
|
+
// Marker carries requestId; optionId is returned via action.option.
|
|
638
|
+
value: {
|
|
639
|
+
[ASK_BUTTON_VALUE_MARKER]: true,
|
|
640
|
+
requestId: request.requestId,
|
|
641
|
+
__larkSelect: true
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
]
|
|
645
|
+
});
|
|
646
|
+
} else {
|
|
647
|
+
const buttons = request.options.map((opt) => ({
|
|
648
|
+
tag: "button",
|
|
649
|
+
text: { tag: "plain_text", content: opt.label },
|
|
650
|
+
type: opt.style ?? "default",
|
|
651
|
+
value: {
|
|
652
|
+
[ASK_BUTTON_VALUE_MARKER]: true,
|
|
653
|
+
requestId: request.requestId,
|
|
654
|
+
optionId: opt.id
|
|
655
|
+
},
|
|
656
|
+
...opt.description ? {
|
|
657
|
+
confirm: {
|
|
658
|
+
title: { tag: "plain_text", content: opt.label },
|
|
659
|
+
text: { tag: "plain_text", content: opt.description }
|
|
660
|
+
}
|
|
661
|
+
} : {}
|
|
662
|
+
}));
|
|
663
|
+
elements.push({ tag: "action", actions: buttons });
|
|
664
|
+
}
|
|
584
665
|
}
|
|
585
666
|
if (request.allowFreeform) {
|
|
586
|
-
const hint =
|
|
667
|
+
const hint = optionCount > 0 ? "_\u2026or reply to this chat with your own answer_" : "_Reply to this chat with your answer_";
|
|
587
668
|
elements.push({ tag: "div", text: { tag: "lark_md", content: hint } });
|
|
588
669
|
}
|
|
589
670
|
return { config: { ...BASE_CONFIG }, elements };
|
|
@@ -879,7 +960,11 @@ function resolveOptions(options, env = defaultEnv()) {
|
|
|
879
960
|
fetch: options.fetch ?? globalThis.fetch,
|
|
880
961
|
ackReaction: options.ackReaction ?? DEFAULTS.ackReaction,
|
|
881
962
|
mode,
|
|
882
|
-
port: options.port ?? (process.env.PORT ? Number(process.env.PORT) : 2e3)
|
|
963
|
+
port: options.port ?? (process.env.PORT ? Number(process.env.PORT) : 2e3),
|
|
964
|
+
allowFrom: options.allowFrom,
|
|
965
|
+
groupAllowFrom: options.groupAllowFrom,
|
|
966
|
+
groupConfigs: options.groupConfigs,
|
|
967
|
+
asrProvider: options.asrProvider
|
|
883
968
|
};
|
|
884
969
|
}
|
|
885
970
|
__name(resolveOptions, "resolveOptions");
|
|
@@ -1327,6 +1412,30 @@ function buildUserContent(text, files, options, messageId) {
|
|
|
1327
1412
|
return parts;
|
|
1328
1413
|
}
|
|
1329
1414
|
__name(buildUserContent, "buildUserContent");
|
|
1415
|
+
async function runDiagnostics(client, opts, chatId) {
|
|
1416
|
+
const lines = ["**eve-lark diagnostics**", ""];
|
|
1417
|
+
lines.push(`appId: \`${opts.appId}\``);
|
|
1418
|
+
lines.push(`baseUrl: \`${opts.baseUrl}\``);
|
|
1419
|
+
lines.push(`mode: \`${opts.mode}\``);
|
|
1420
|
+
lines.push(`replyMode: \`${opts.replyMode}\``);
|
|
1421
|
+
lines.push(`encryptKey: ${opts.encryptKey ? "\u2713 set" : "\u2717 not set"}`);
|
|
1422
|
+
lines.push(`ackReaction: \`${opts.ackReaction === false ? "disabled" : opts.ackReaction}\``);
|
|
1423
|
+
lines.push("");
|
|
1424
|
+
lines.push("**Token fetch:**");
|
|
1425
|
+
try {
|
|
1426
|
+
const token = await client.getTenantAccessToken();
|
|
1427
|
+
lines.push(`\u2713 tenant_access_token: ${token.slice(0, 8)}\u2026`);
|
|
1428
|
+
} catch (e) {
|
|
1429
|
+
lines.push(`\u2717 failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
1430
|
+
}
|
|
1431
|
+
const report = lines.join("\n");
|
|
1432
|
+
try {
|
|
1433
|
+
await client.sendPost({ chatId, content: report });
|
|
1434
|
+
} catch (e) {
|
|
1435
|
+
console.error("[eve-lark] diagnostic report delivery failed:", e);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
__name(runDiagnostics, "runDiagnostics");
|
|
1330
1439
|
function formatErrorHint(data) {
|
|
1331
1440
|
if (typeof data !== "object" || data === null) return "";
|
|
1332
1441
|
const d = data;
|
|
@@ -1376,6 +1485,7 @@ function createLarkChannel(optionsInput) {
|
|
|
1376
1485
|
const sessionMeta = /* @__PURE__ */ new Map();
|
|
1377
1486
|
const pendingInputsByRequestId = /* @__PURE__ */ new Map();
|
|
1378
1487
|
const pendingInputsByChatToken = /* @__PURE__ */ new Map();
|
|
1488
|
+
const authCards = /* @__PURE__ */ new Map();
|
|
1379
1489
|
function getController(sessionId, meta) {
|
|
1380
1490
|
let ctrl = controllers.get(sessionId);
|
|
1381
1491
|
if (!ctrl) {
|
|
@@ -1605,9 +1715,55 @@ function createLarkChannel(optionsInput) {
|
|
|
1605
1715
|
if (parsed.senderType === "app") {
|
|
1606
1716
|
return ackOk();
|
|
1607
1717
|
}
|
|
1718
|
+
if (parsed.chatType === "p2p" && options.allowFrom) {
|
|
1719
|
+
if (!options.allowFrom.includes(parsed.senderOpenId)) {
|
|
1720
|
+
console.log(
|
|
1721
|
+
`[eve-lark] dropping DM from non-allowlisted sender ${parsed.senderOpenId}`
|
|
1722
|
+
);
|
|
1723
|
+
return ackOk();
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (parsed.chatType === "group" && options.groupAllowFrom) {
|
|
1727
|
+
if (!options.groupAllowFrom.includes(parsed.chatId)) {
|
|
1728
|
+
console.log(
|
|
1729
|
+
`[eve-lark] dropping group message from non-allowlisted chat ${parsed.chatId}`
|
|
1730
|
+
);
|
|
1731
|
+
return ackOk();
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
if (options.asrProvider && parsed.text === "" && parsed.files.length === 0) {
|
|
1735
|
+
const rawEvent = body.event;
|
|
1736
|
+
const msgType = rawEvent.message?.message_type;
|
|
1737
|
+
if (msgType === "audio" || msgType === "media") {
|
|
1738
|
+
try {
|
|
1739
|
+
const content = JSON.parse(rawEvent.message.content);
|
|
1740
|
+
if (content.file_key) {
|
|
1741
|
+
const bytes = await client.downloadResource({
|
|
1742
|
+
messageId: parsed.messageId,
|
|
1743
|
+
fileKey: content.file_key,
|
|
1744
|
+
type: "file"
|
|
1745
|
+
});
|
|
1746
|
+
const mediaType = msgType === "audio" ? "audio/mpeg" : "video/mp4";
|
|
1747
|
+
const transcript = await options.asrProvider.transcribe(bytes, mediaType);
|
|
1748
|
+
if (transcript) {
|
|
1749
|
+
parsed.text = transcript;
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
} catch (e) {
|
|
1753
|
+
console.warn(
|
|
1754
|
+
"[eve-lark] audio transcription failed, skipping message:",
|
|
1755
|
+
e instanceof Error ? e.message : e
|
|
1756
|
+
);
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1608
1760
|
if (parsed.text === "" && parsed.files.length === 0) {
|
|
1609
1761
|
return ackOk();
|
|
1610
1762
|
}
|
|
1763
|
+
if (parsed.text.trim().toLowerCase() === "/lark-diagnose") {
|
|
1764
|
+
helpers.waitUntil(runDiagnostics(client, options, parsed.chatId));
|
|
1765
|
+
return ackOk();
|
|
1766
|
+
}
|
|
1611
1767
|
const tokenKey = chatTokenKey(parsed.chatId, parsed.rootId ?? void 0, parsed.parentId ?? void 0);
|
|
1612
1768
|
const pending = pendingInputsByChatToken.get(tokenKey);
|
|
1613
1769
|
if (pending && pending.awaitingFreeform && parsed.text.length > 0) {
|
|
@@ -1659,10 +1815,15 @@ function createLarkChannel(optionsInput) {
|
|
|
1659
1815
|
chatType: parsed.chatType
|
|
1660
1816
|
}
|
|
1661
1817
|
};
|
|
1662
|
-
const
|
|
1818
|
+
const groupConfig = parsed.chatType === "group" ? options.groupConfigs?.find((g) => g.chatId === parsed.chatId) : void 0;
|
|
1819
|
+
const sendPayload = {
|
|
1663
1820
|
auth,
|
|
1664
1821
|
continuationToken
|
|
1665
|
-
}
|
|
1822
|
+
};
|
|
1823
|
+
if (groupConfig?.systemPrompt) {
|
|
1824
|
+
sendPayload.context = [groupConfig.systemPrompt];
|
|
1825
|
+
}
|
|
1826
|
+
const session = await helpers.send(userContent, sendPayload);
|
|
1666
1827
|
sessionMeta.set(session.id, {
|
|
1667
1828
|
chatId: parsed.chatId,
|
|
1668
1829
|
rootId: parsed.rootId ?? void 0,
|
|
@@ -1693,50 +1854,69 @@ function createLarkChannel(optionsInput) {
|
|
|
1693
1854
|
return ackOk();
|
|
1694
1855
|
}
|
|
1695
1856
|
const requestId = typeof value.requestId === "string" ? value.requestId : "";
|
|
1696
|
-
const optionId = typeof value.optionId === "string" ? value.optionId : "";
|
|
1857
|
+
const optionId = (typeof value.optionId === "string" ? value.optionId : "") || (typeof evt.action?.option === "string" ? evt.action.option : "");
|
|
1697
1858
|
if (!requestId) return ackOk();
|
|
1698
1859
|
const pending = pendingInputsByRequestId.get(requestId);
|
|
1699
1860
|
if (!pending) {
|
|
1700
|
-
console.warn(
|
|
1701
|
-
|
|
1702
|
-
}
|
|
1703
|
-
const resp = { requestId, optionId: optionId || void 0 };
|
|
1704
|
-
const resumeToken = larkContinuationToken(pending.chatId, pending.parentId ?? pending.rootId ?? null);
|
|
1705
|
-
const resumeAuth = {
|
|
1706
|
-
authenticator: "lark",
|
|
1707
|
-
principalType: "user",
|
|
1708
|
-
principalId: evt.open_id,
|
|
1709
|
-
attributes: {
|
|
1710
|
-
chatId: pending.chatId,
|
|
1711
|
-
rootMessageId: pending.rootId,
|
|
1712
|
-
messageId: evt.open_message_id,
|
|
1713
|
-
chatType: pending.request.display === "confirmation" ? "p2p" : "group"
|
|
1714
|
-
}
|
|
1715
|
-
};
|
|
1716
|
-
try {
|
|
1717
|
-
await helpers.send(
|
|
1718
|
-
{ inputResponses: [resp] },
|
|
1719
|
-
{ auth: resumeAuth, continuationToken: resumeToken }
|
|
1720
|
-
);
|
|
1721
|
-
console.log(`[eve-lark] ask answered via button click requestId=${requestId} optionId=${optionId}`);
|
|
1722
|
-
} catch (e) {
|
|
1723
|
-
console.error(
|
|
1724
|
-
`[eve-lark] ask input-response send failed (requestId=${requestId}):`,
|
|
1725
|
-
e instanceof Error ? e.message : e
|
|
1861
|
+
console.warn(
|
|
1862
|
+
`[eve-lark] card action for unknown requestId=${requestId} (already answered or expired)`
|
|
1726
1863
|
);
|
|
1864
|
+
return ackOk();
|
|
1727
1865
|
}
|
|
1728
1866
|
const selectedOpt = pending.request.options?.find((o) => o.id === optionId);
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1867
|
+
helpers.waitUntil(
|
|
1868
|
+
(async () => {
|
|
1869
|
+
if (pending.cardMessageId && selectedOpt) {
|
|
1870
|
+
try {
|
|
1871
|
+
await client.patchCard({
|
|
1872
|
+
messageId: pending.cardMessageId,
|
|
1873
|
+
card: buildAskAnsweredCard(pending.request, {
|
|
1874
|
+
kind: "option",
|
|
1875
|
+
label: selectedOpt.label
|
|
1876
|
+
})
|
|
1877
|
+
});
|
|
1878
|
+
} catch (e) {
|
|
1879
|
+
console.warn(
|
|
1880
|
+
"[eve-lark] patchCard after ask-answer failed:",
|
|
1881
|
+
e instanceof Error ? e.message : e
|
|
1882
|
+
);
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
const resp = { requestId, optionId: optionId || void 0 };
|
|
1886
|
+
const resumeToken = larkContinuationToken(
|
|
1887
|
+
pending.chatId,
|
|
1888
|
+
pending.parentId ?? pending.rootId ?? null
|
|
1889
|
+
);
|
|
1890
|
+
const resumeAuth = {
|
|
1891
|
+
authenticator: "lark",
|
|
1892
|
+
principalType: "user",
|
|
1893
|
+
principalId: evt.open_id,
|
|
1894
|
+
attributes: {
|
|
1895
|
+
chatId: pending.chatId,
|
|
1896
|
+
rootMessageId: pending.rootId,
|
|
1897
|
+
messageId: evt.open_message_id,
|
|
1898
|
+
chatType: pending.request.display === "confirmation" ? "p2p" : "group"
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1901
|
+
try {
|
|
1902
|
+
await helpers.send(
|
|
1903
|
+
{ inputResponses: [resp] },
|
|
1904
|
+
{ auth: resumeAuth, continuationToken: resumeToken }
|
|
1905
|
+
);
|
|
1906
|
+
console.log(
|
|
1907
|
+
`[eve-lark] ask answered via card action requestId=${requestId} optionId=${optionId}`
|
|
1908
|
+
);
|
|
1909
|
+
} catch (e) {
|
|
1910
|
+
console.error(
|
|
1911
|
+
`[eve-lark] ask input-response send failed (requestId=${requestId}):`,
|
|
1912
|
+
e instanceof Error ? e.message : e
|
|
1913
|
+
);
|
|
1914
|
+
}
|
|
1915
|
+
dropPendingInput(pending);
|
|
1916
|
+
})().catch((e) => {
|
|
1917
|
+
console.error("[eve-lark] card action background work failed:", e);
|
|
1918
|
+
})
|
|
1919
|
+
);
|
|
1740
1920
|
return ackOk();
|
|
1741
1921
|
}
|
|
1742
1922
|
__name(handleCardAction, "handleCardAction");
|
|
@@ -1752,6 +1932,31 @@ function createLarkChannel(optionsInput) {
|
|
|
1752
1932
|
const ctrl = getController(sessionId, info);
|
|
1753
1933
|
ctrl.appendDelta(d.messageDelta);
|
|
1754
1934
|
},
|
|
1935
|
+
// Model is about to call tools. Update the streaming card status so the
|
|
1936
|
+
// user sees what's happening mid-turn instead of a static typing dot.
|
|
1937
|
+
// Only fires when replyMode is "streaming" (cards exist). Post/static
|
|
1938
|
+
// modes have no live surface to update.
|
|
1939
|
+
async "actions.requested"(data, _channel, ctx) {
|
|
1940
|
+
if (options.replyMode !== "streaming") return;
|
|
1941
|
+
const sessionId = ctx.session.id;
|
|
1942
|
+
const ctrl = controllers.get(sessionId);
|
|
1943
|
+
if (!ctrl) return;
|
|
1944
|
+
const d = data;
|
|
1945
|
+
const names = (d.actions ?? []).map((a) => a.toolName).filter((n) => typeof n === "string");
|
|
1946
|
+
if (names.length === 0) return;
|
|
1947
|
+
const label = names.length === 1 ? `\u{1F527} ${names[0]}` : `\u{1F527} ${names.join(", ")}`;
|
|
1948
|
+
ctrl.setStatus(label);
|
|
1949
|
+
},
|
|
1950
|
+
// A tool finished. Clear the status (the next message.appended or
|
|
1951
|
+
// message.completed will overwrite anyway, but clearing here gives
|
|
1952
|
+
// snappier feedback for long tool chains). Best-effort.
|
|
1953
|
+
async "action.result"(_data, _channel, ctx) {
|
|
1954
|
+
if (options.replyMode !== "streaming") return;
|
|
1955
|
+
const sessionId = ctx.session.id;
|
|
1956
|
+
const ctrl = controllers.get(sessionId);
|
|
1957
|
+
if (!ctrl) return;
|
|
1958
|
+
ctrl.setStatus("");
|
|
1959
|
+
},
|
|
1755
1960
|
// eve's ask_question (and similar HITL tools) fire this event with a
|
|
1756
1961
|
// list of input requests. Each request becomes a Feishu card with
|
|
1757
1962
|
// buttons (one per option) plus optional freeform hint.
|
|
@@ -1872,6 +2077,67 @@ function createLarkChannel(optionsInput) {
|
|
|
1872
2077
|
console.error(
|
|
1873
2078
|
`[eve-lark] session.failed: ${userText}` + (errorId ? ` (errorId=${errorId})` : "")
|
|
1874
2079
|
);
|
|
2080
|
+
},
|
|
2081
|
+
// Turn ended cleanly. eve fires this after the final message.completed
|
|
2082
|
+
// (or instead of it when the assistant step ended in tool-calls with no
|
|
2083
|
+
// visible text). Either way, free this session's controller + ack
|
|
2084
|
+
// reaction so we don't leak waiting for a message.completed that's
|
|
2085
|
+
// never coming.
|
|
2086
|
+
async "turn.completed"(data, _channel, ctx) {
|
|
2087
|
+
const sessionId = ctx?.session?.id;
|
|
2088
|
+
if (!sessionId) return;
|
|
2089
|
+
try {
|
|
2090
|
+
await cleanupAckReaction(sessionId);
|
|
2091
|
+
} catch {
|
|
2092
|
+
}
|
|
2093
|
+
dropController(sessionId);
|
|
2094
|
+
},
|
|
2095
|
+
// The agent needs the user to sign in to an external service (e.g.
|
|
2096
|
+
// GitHub, Slack, Linear). Render a card with a "Sign in with <X>"
|
|
2097
|
+
// URL button so the user can complete the flow in their browser.
|
|
2098
|
+
// The card message id is tracked so `authorization.completed` can
|
|
2099
|
+
// patch it with the outcome.
|
|
2100
|
+
async "authorization.required"(data, _channel, ctx) {
|
|
2101
|
+
const sessionId = ctx?.session?.id;
|
|
2102
|
+
const info = sessionInfoFromCtx(ctx);
|
|
2103
|
+
if (!info || !sessionId) return;
|
|
2104
|
+
const d = data;
|
|
2105
|
+
const name = d.name ?? "service";
|
|
2106
|
+
const displayName = d.authorization?.displayName ?? name;
|
|
2107
|
+
const url = d.authorization?.url;
|
|
2108
|
+
if (!url) {
|
|
2109
|
+
console.warn(`[eve-lark] authorization.required for ${name}: no url, skipping card`);
|
|
2110
|
+
return;
|
|
2111
|
+
}
|
|
2112
|
+
const card = buildAuthCard({ displayName, url, userCode: d.authorization?.userCode });
|
|
2113
|
+
try {
|
|
2114
|
+
const res = await client.sendCard({ chatId: info.chatId, card, rootId: info.rootId, parentId: info.parentId });
|
|
2115
|
+
authCards.set(`${sessionId}:${name}`, res.messageId);
|
|
2116
|
+
} catch (e) {
|
|
2117
|
+
console.error(`[eve-lark] auth card send failed (${name}):`, e instanceof Error ? e.message : e);
|
|
2118
|
+
}
|
|
2119
|
+
},
|
|
2120
|
+
// The user completed (or declined) the external auth. Patch the card
|
|
2121
|
+
// we rendered in `authorization.required` to show the outcome.
|
|
2122
|
+
async "authorization.completed"(data, _channel, ctx) {
|
|
2123
|
+
const sessionId = ctx?.session?.id;
|
|
2124
|
+
if (!sessionId) return;
|
|
2125
|
+
const d = data;
|
|
2126
|
+
const name = d.name ?? "service";
|
|
2127
|
+
const cardMessageId = authCards.get(`${sessionId}:${name}`);
|
|
2128
|
+
if (!cardMessageId) return;
|
|
2129
|
+
const displayName = d.authorization?.displayName ?? name;
|
|
2130
|
+
const card = buildAuthCompletedCard({
|
|
2131
|
+
displayName,
|
|
2132
|
+
outcome: d.outcome ?? "completed",
|
|
2133
|
+
reason: d.reason
|
|
2134
|
+
});
|
|
2135
|
+
try {
|
|
2136
|
+
await client.patchCard({ messageId: cardMessageId, card });
|
|
2137
|
+
} catch (e) {
|
|
2138
|
+
console.warn(`[eve-lark] auth card patch failed (${name}):`, e instanceof Error ? e.message : e);
|
|
2139
|
+
}
|
|
2140
|
+
authCards.delete(`${sessionId}:${name}`);
|
|
1875
2141
|
}
|
|
1876
2142
|
};
|
|
1877
2143
|
const channel = defineChannel({
|