eve-lark 0.4.5 → 0.5.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 +17 -16
- package/README.zh-CN.md +17 -13
- package/dist/index.js +209 -107
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -107,22 +107,24 @@ var LarkClient = class {
|
|
|
107
107
|
}
|
|
108
108
|
async sendText(args) {
|
|
109
109
|
const content = JSON.stringify({ text: args.content });
|
|
110
|
+
if (args.rootId) {
|
|
111
|
+
return this.#replyMessage(args.rootId, "text", content);
|
|
112
|
+
}
|
|
110
113
|
return this.#sendMessage({
|
|
111
114
|
receive_id: args.chatId,
|
|
112
115
|
msg_type: "text",
|
|
113
|
-
content
|
|
114
|
-
root_id: args.rootId,
|
|
115
|
-
parent_id: args.parentId
|
|
116
|
+
content
|
|
116
117
|
});
|
|
117
118
|
}
|
|
118
119
|
async sendCard(args) {
|
|
119
120
|
const content = JSON.stringify(args.card);
|
|
121
|
+
if (args.rootId) {
|
|
122
|
+
return this.#replyMessage(args.rootId, "interactive", content);
|
|
123
|
+
}
|
|
120
124
|
return this.#sendMessage({
|
|
121
125
|
receive_id: args.chatId,
|
|
122
126
|
msg_type: "interactive",
|
|
123
|
-
content
|
|
124
|
-
root_id: args.rootId,
|
|
125
|
-
parent_id: args.parentId
|
|
127
|
+
content
|
|
126
128
|
});
|
|
127
129
|
}
|
|
128
130
|
async sendPost(args) {
|
|
@@ -131,14 +133,31 @@ var LarkClient = class {
|
|
|
131
133
|
content: [[{ tag: "md", text: args.content }]]
|
|
132
134
|
}
|
|
133
135
|
};
|
|
136
|
+
const content = JSON.stringify(post);
|
|
137
|
+
if (args.rootId) {
|
|
138
|
+
return this.#replyMessage(args.rootId, "post", content);
|
|
139
|
+
}
|
|
134
140
|
return this.#sendMessage({
|
|
135
141
|
receive_id: args.chatId,
|
|
136
142
|
msg_type: "post",
|
|
137
|
-
content
|
|
138
|
-
root_id: args.rootId,
|
|
139
|
-
parent_id: args.parentId
|
|
143
|
+
content
|
|
140
144
|
});
|
|
141
145
|
}
|
|
146
|
+
/** Quote-reply to a specific message via Feishu's reply API.
|
|
147
|
+
* POST /open-apis/im/v1/messages/{message_id}/reply — this is the only
|
|
148
|
+
* way to quote-reply to a normal (non-thread) message; sendMessage's
|
|
149
|
+
* root_id field only works inside threads. */
|
|
150
|
+
async #replyMessage(replyToMessageId, msgType, content) {
|
|
151
|
+
const path = `/open-apis/im/v1/messages/${encodeURIComponent(replyToMessageId)}/reply`;
|
|
152
|
+
const json = await this.#request("POST", path, { msg_type: msgType, content });
|
|
153
|
+
const messageId = json.data?.message_id;
|
|
154
|
+
if (!messageId) {
|
|
155
|
+
throw new LarkApiError("eve-lark: reply missing message_id in response", {
|
|
156
|
+
body: json
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
return { messageId };
|
|
160
|
+
}
|
|
142
161
|
async #sendMessage(body) {
|
|
143
162
|
const payload = Object.fromEntries(
|
|
144
163
|
Object.entries(body).filter(([, v]) => v !== void 0)
|
|
@@ -885,6 +904,16 @@ var StreamingCardController = class {
|
|
|
885
904
|
getMessageId() {
|
|
886
905
|
return this.messageId;
|
|
887
906
|
}
|
|
907
|
+
/** Aim the next card we create at this user message (quote-reply). Used
|
|
908
|
+
* when messages interleave so the user can tell which reply answers which
|
|
909
|
+
* message. Must be called before the first sendCard (i.e. in the webhook
|
|
910
|
+
* handler right after helpers.send, before any streaming delta).
|
|
911
|
+
*
|
|
912
|
+
* Sets rootId (not parentId) because Feishu's sendMessage API uses root_id
|
|
913
|
+
* for quote-reply; parent_id is ignored. */
|
|
914
|
+
setReplyTarget(messageId) {
|
|
915
|
+
this.deps.rootId = messageId;
|
|
916
|
+
}
|
|
888
917
|
/** Active HITL request being rendered inline on this card, or null. */
|
|
889
918
|
getAskRequest() {
|
|
890
919
|
return this.askRequest;
|
|
@@ -946,10 +975,11 @@ var StreamingCardController = class {
|
|
|
946
975
|
* patch the first card — the user would only see the latest reply
|
|
947
976
|
* overwritten onto one card instead of N independent cards.
|
|
948
977
|
*/
|
|
949
|
-
resetForNewMessage() {
|
|
978
|
+
resetForNewMessage(replyTargetMessageId) {
|
|
950
979
|
this.resetForNewTurn();
|
|
951
980
|
this.messageId = void 0;
|
|
952
981
|
this.state = "idle";
|
|
982
|
+
if (replyTargetMessageId) this.deps.rootId = replyTargetMessageId;
|
|
953
983
|
}
|
|
954
984
|
async finalize(fullText) {
|
|
955
985
|
if (this.state === "completed" || this.state === "aborted") return;
|
|
@@ -1032,9 +1062,10 @@ var StreamingCardController = class {
|
|
|
1032
1062
|
scheduleCreate() {
|
|
1033
1063
|
if (this.createTimer) return;
|
|
1034
1064
|
this.state = "creating";
|
|
1065
|
+
const rootIdSnapshot = this.deps.rootId;
|
|
1035
1066
|
this.createTimer = setTimeout(() => {
|
|
1036
1067
|
this.createTimer = null;
|
|
1037
|
-
void this.doCreate();
|
|
1068
|
+
void this.doCreate(rootIdSnapshot);
|
|
1038
1069
|
}, this.deps.createThresholdMs);
|
|
1039
1070
|
}
|
|
1040
1071
|
cancelCreateTimer() {
|
|
@@ -1043,13 +1074,13 @@ var StreamingCardController = class {
|
|
|
1043
1074
|
this.createTimer = null;
|
|
1044
1075
|
}
|
|
1045
1076
|
}
|
|
1046
|
-
async doCreate() {
|
|
1077
|
+
async doCreate(rootIdSnapshot) {
|
|
1047
1078
|
if (this.state !== "creating") return;
|
|
1048
1079
|
try {
|
|
1049
1080
|
const res = await this.client.sendCard({
|
|
1050
1081
|
chatId: this.deps.chatId,
|
|
1051
1082
|
card: this.deps.useCardKitV2 ? buildCardKitStreamingCard({ buffer: this.buffer, status: this.status, streamingMode: true, toolCalls: this.toolCalls, askRequest: this.askRequest }) : buildStreamingCard({ buffer: this.buffer, status: this.status, toolCalls: this.toolCalls, askRequest: this.askRequest }),
|
|
1052
|
-
rootId: this.deps.rootId,
|
|
1083
|
+
rootId: rootIdSnapshot ?? this.deps.rootId,
|
|
1053
1084
|
parentId: this.deps.parentId
|
|
1054
1085
|
});
|
|
1055
1086
|
this.messageId = res.messageId;
|
|
@@ -1104,12 +1135,11 @@ var StreamingCardController = class {
|
|
|
1104
1135
|
var DEFAULTS = {
|
|
1105
1136
|
baseUrl: "https://open.feishu.cn",
|
|
1106
1137
|
webhookPath: "/lark/webhook",
|
|
1107
|
-
// "
|
|
1108
|
-
//
|
|
1109
|
-
//
|
|
1110
|
-
//
|
|
1111
|
-
|
|
1112
|
-
replyMode: "post",
|
|
1138
|
+
// "streaming-v2" uses Feishu CardKit v2 (schema 2.0 + streaming_mode) for
|
|
1139
|
+
// live-patched interactive cards — the best live UX this channel can offer
|
|
1140
|
+
// today. Users who prefer native chat-message size (with markdown) at the
|
|
1141
|
+
// cost of no streaming should opt into replyMode: "post".
|
|
1142
|
+
replyMode: "streaming-v2",
|
|
1113
1143
|
streamPatchIntervalMs: 1e3,
|
|
1114
1144
|
streamCreateThresholdMs: 400,
|
|
1115
1145
|
dedupTtlMs: 30 * 60 * 1e3,
|
|
@@ -1712,13 +1742,18 @@ function createLarkChannel(optionsInput) {
|
|
|
1712
1742
|
console.log("[eve-lark] skipping WSClient start in eve-start launcher process; the spawned server will start it");
|
|
1713
1743
|
}
|
|
1714
1744
|
const controllers = /* @__PURE__ */ new Map();
|
|
1745
|
+
const turnSources = /* @__PURE__ */ new Map();
|
|
1746
|
+
const turnTouchedAt = /* @__PURE__ */ new Map();
|
|
1715
1747
|
const sessionMeta = /* @__PURE__ */ new Map();
|
|
1716
1748
|
const pendingInputsByRequestId = /* @__PURE__ */ new Map();
|
|
1717
1749
|
const pendingInputsByChatToken = /* @__PURE__ */ new Map();
|
|
1718
1750
|
const authCards = /* @__PURE__ */ new Map();
|
|
1719
|
-
const
|
|
1720
|
-
|
|
1721
|
-
|
|
1751
|
+
const ackReactions = /* @__PURE__ */ new Map();
|
|
1752
|
+
const currentInboundMsgIds = /* @__PURE__ */ new Map();
|
|
1753
|
+
const lastChatMessage = /* @__PURE__ */ new Map();
|
|
1754
|
+
const cleanedTurns = /* @__PURE__ */ new Set();
|
|
1755
|
+
function getController(turnId, meta) {
|
|
1756
|
+
let ctrl = controllers.get(turnId);
|
|
1722
1757
|
if (!ctrl) {
|
|
1723
1758
|
ctrl = new StreamingCardController(client, {
|
|
1724
1759
|
chatId: meta.chatId,
|
|
@@ -1728,39 +1763,48 @@ function createLarkChannel(optionsInput) {
|
|
|
1728
1763
|
createThresholdMs: options.streamCreateThresholdMs,
|
|
1729
1764
|
useCardKitV2: options.replyMode === "streaming-v2"
|
|
1730
1765
|
});
|
|
1731
|
-
controllers.set(
|
|
1732
|
-
}
|
|
1733
|
-
if (sessionMeta.has(sessionId)) {
|
|
1734
|
-
sessionMeta.get(sessionId).touchedAt = Date.now();
|
|
1735
|
-
} else {
|
|
1736
|
-
sessionMeta.set(sessionId, {
|
|
1737
|
-
chatId: meta.chatId,
|
|
1738
|
-
rootId: meta.rootId,
|
|
1739
|
-
parentId: meta.parentId,
|
|
1740
|
-
messageId: meta.messageId,
|
|
1741
|
-
touchedAt: Date.now()
|
|
1742
|
-
});
|
|
1766
|
+
controllers.set(turnId, ctrl);
|
|
1743
1767
|
}
|
|
1768
|
+
turnTouchedAt.set(turnId, Date.now());
|
|
1744
1769
|
return ctrl;
|
|
1745
1770
|
}
|
|
1746
1771
|
__name(getController, "getController");
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1772
|
+
function dropTurn(turnId) {
|
|
1773
|
+
controllers.delete(turnId);
|
|
1774
|
+
turnSources.delete(turnId);
|
|
1775
|
+
turnTouchedAt.delete(turnId);
|
|
1776
|
+
}
|
|
1777
|
+
__name(dropTurn, "dropTurn");
|
|
1778
|
+
async function cleanupAckReaction(messageId) {
|
|
1779
|
+
const ack = ackReactions.get(messageId);
|
|
1780
|
+
if (!ack) return;
|
|
1750
1781
|
try {
|
|
1751
|
-
await client.removeReaction({
|
|
1752
|
-
messageId: meta.messageId,
|
|
1753
|
-
reactionId: meta.ackReactionId
|
|
1754
|
-
});
|
|
1782
|
+
await client.removeReaction({ messageId, reactionId: ack.reactionId });
|
|
1755
1783
|
} catch (e) {
|
|
1756
1784
|
console.warn(
|
|
1757
1785
|
"[eve-lark] ack reaction cleanup failed:",
|
|
1758
1786
|
e instanceof Error ? e.message : e
|
|
1759
1787
|
);
|
|
1760
1788
|
}
|
|
1789
|
+
ackReactions.delete(messageId);
|
|
1761
1790
|
}
|
|
1762
1791
|
__name(cleanupAckReaction, "cleanupAckReaction");
|
|
1763
|
-
async function
|
|
1792
|
+
async function cleanupAckForSession(sessionId) {
|
|
1793
|
+
const queue = currentInboundMsgIds.get(sessionId);
|
|
1794
|
+
if (!queue || queue.length === 0) return;
|
|
1795
|
+
const msgId = queue.shift();
|
|
1796
|
+
if (!msgId) return;
|
|
1797
|
+
if (queue.length === 0) currentInboundMsgIds.delete(sessionId);
|
|
1798
|
+
await cleanupAckReaction(msgId);
|
|
1799
|
+
}
|
|
1800
|
+
__name(cleanupAckForSession, "cleanupAckForSession");
|
|
1801
|
+
async function cleanupAckForTurn(turnId) {
|
|
1802
|
+
const sourceMsgId = turnSources.get(turnId);
|
|
1803
|
+
if (!sourceMsgId) return;
|
|
1804
|
+
await cleanupAckReaction(sourceMsgId);
|
|
1805
|
+
}
|
|
1806
|
+
__name(cleanupAckForTurn, "cleanupAckForTurn");
|
|
1807
|
+
async function deliverReply(turnId, info, text) {
|
|
1764
1808
|
if (options.replyMode === "post") {
|
|
1765
1809
|
try {
|
|
1766
1810
|
await client.sendPost({
|
|
@@ -1769,11 +1813,11 @@ function createLarkChannel(optionsInput) {
|
|
|
1769
1813
|
rootId: info.rootId,
|
|
1770
1814
|
parentId: info.parentId
|
|
1771
1815
|
});
|
|
1772
|
-
console.log(`[eve-lark] delivered via sendPost (
|
|
1816
|
+
console.log(`[eve-lark] delivered via sendPost (turnId=${turnId ?? "?"})`);
|
|
1773
1817
|
return;
|
|
1774
1818
|
} catch (postErr) {
|
|
1775
1819
|
console.warn(
|
|
1776
|
-
`[eve-lark] sendPost failed; falling back to plain text (
|
|
1820
|
+
`[eve-lark] sendPost failed; falling back to plain text (turnId=${turnId ?? "?"}):`,
|
|
1777
1821
|
postErr instanceof Error ? postErr.message : postErr
|
|
1778
1822
|
);
|
|
1779
1823
|
}
|
|
@@ -1784,40 +1828,43 @@ function createLarkChannel(optionsInput) {
|
|
|
1784
1828
|
rootId: info.rootId,
|
|
1785
1829
|
parentId: info.parentId
|
|
1786
1830
|
});
|
|
1787
|
-
console.log(`[eve-lark] delivered via sendText fallback (
|
|
1831
|
+
console.log(`[eve-lark] delivered via sendText fallback (turnId=${turnId ?? "?"})`);
|
|
1788
1832
|
} catch (textErr) {
|
|
1789
1833
|
console.error(
|
|
1790
|
-
`[eve-lark] sendText fallback ALSO failed; the user will not see this reply (
|
|
1834
|
+
`[eve-lark] sendText fallback ALSO failed; the user will not see this reply (turnId=${turnId ?? "?"}):`,
|
|
1791
1835
|
textErr instanceof Error ? textErr.message : textErr
|
|
1792
1836
|
);
|
|
1793
1837
|
}
|
|
1794
1838
|
return;
|
|
1795
1839
|
}
|
|
1796
1840
|
if (options.replyMode === "streaming" || options.replyMode === "streaming-v2") {
|
|
1797
|
-
const ctrl = controllers.get(
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1841
|
+
const ctrl = turnId ? controllers.get(turnId) : void 0;
|
|
1842
|
+
if (ctrl) {
|
|
1843
|
+
try {
|
|
1844
|
+
await ctrl.finalize(text);
|
|
1845
|
+
console.log(`[eve-lark] delivered via streaming finalize (turnId=${turnId})`);
|
|
1846
|
+
return;
|
|
1847
|
+
} catch (e) {
|
|
1848
|
+
console.warn(
|
|
1849
|
+
`[eve-lark] streaming finalize failed; falling back to fresh card (turnId=${turnId}):`,
|
|
1850
|
+
e instanceof Error ? e.message : e
|
|
1851
|
+
);
|
|
1852
|
+
}
|
|
1807
1853
|
}
|
|
1808
1854
|
}
|
|
1809
1855
|
try {
|
|
1810
|
-
await client.sendCard({
|
|
1856
|
+
const res = await client.sendCard({
|
|
1811
1857
|
chatId: info.chatId,
|
|
1812
1858
|
card: buildTextCard(text),
|
|
1813
1859
|
rootId: info.rootId,
|
|
1814
1860
|
parentId: info.parentId
|
|
1815
1861
|
});
|
|
1816
|
-
console.log(`[eve-lark] delivered via sendCard (
|
|
1862
|
+
console.log(`[eve-lark] delivered via sendCard (turnId=${turnId ?? "?"})`);
|
|
1863
|
+
void res;
|
|
1817
1864
|
return;
|
|
1818
1865
|
} catch (cardErr) {
|
|
1819
1866
|
console.warn(
|
|
1820
|
-
`[eve-lark] sendCard failed; falling back to plain text (
|
|
1867
|
+
`[eve-lark] sendCard failed; falling back to plain text (turnId=${turnId ?? "?"}):`,
|
|
1821
1868
|
cardErr instanceof Error ? cardErr.message : cardErr
|
|
1822
1869
|
);
|
|
1823
1870
|
}
|
|
@@ -1828,10 +1875,10 @@ function createLarkChannel(optionsInput) {
|
|
|
1828
1875
|
rootId: info.rootId,
|
|
1829
1876
|
parentId: info.parentId
|
|
1830
1877
|
});
|
|
1831
|
-
console.log(`[eve-lark] delivered via sendText fallback (
|
|
1878
|
+
console.log(`[eve-lark] delivered via sendText fallback (turnId=${turnId ?? "?"})`);
|
|
1832
1879
|
} catch (textErr) {
|
|
1833
1880
|
console.error(
|
|
1834
|
-
`[eve-lark] sendText fallback ALSO failed; the user will not see this reply (
|
|
1881
|
+
`[eve-lark] sendText fallback ALSO failed; the user will not see this reply (turnId=${turnId ?? "?"}):`,
|
|
1835
1882
|
textErr instanceof Error ? textErr.message : textErr
|
|
1836
1883
|
);
|
|
1837
1884
|
}
|
|
@@ -1843,9 +1890,13 @@ function createLarkChannel(optionsInput) {
|
|
|
1843
1890
|
if (now - lastSweepAt < SWEEP_INTERVAL_MS) return;
|
|
1844
1891
|
lastSweepAt = now;
|
|
1845
1892
|
const cutoff = now - STALE_SESSION_MS;
|
|
1893
|
+
for (const [turnId, touchedAt] of turnTouchedAt) {
|
|
1894
|
+
if (touchedAt < cutoff) {
|
|
1895
|
+
dropTurn(turnId);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1846
1898
|
for (const [id, meta] of sessionMeta) {
|
|
1847
1899
|
if (meta.touchedAt < cutoff) {
|
|
1848
|
-
controllers.delete(id);
|
|
1849
1900
|
sessionMeta.delete(id);
|
|
1850
1901
|
}
|
|
1851
1902
|
}
|
|
@@ -2070,21 +2121,22 @@ function createLarkChannel(optionsInput) {
|
|
|
2070
2121
|
console.log(
|
|
2071
2122
|
`[eve-lark] helpers.send returned sessionId=${session.id} for chatId=${parsed.chatId}`
|
|
2072
2123
|
);
|
|
2073
|
-
|
|
2124
|
+
const queue = currentInboundMsgIds.get(session.id);
|
|
2125
|
+
if (queue) queue.push(parsed.messageId);
|
|
2126
|
+
else currentInboundMsgIds.set(session.id, [parsed.messageId]);
|
|
2127
|
+
lastChatMessage.set(session.id, parsed.messageId);
|
|
2074
2128
|
sessionMeta.set(session.id, {
|
|
2075
2129
|
chatId: parsed.chatId,
|
|
2076
2130
|
rootId: parsed.rootId ?? void 0,
|
|
2077
2131
|
parentId: parsed.parentId ?? void 0,
|
|
2078
|
-
messageId: parsed.messageId,
|
|
2079
2132
|
touchedAt: Date.now()
|
|
2080
2133
|
});
|
|
2081
2134
|
const emoji = pickAckEmoji(options.ackReaction);
|
|
2082
2135
|
if (emoji) {
|
|
2083
|
-
const
|
|
2136
|
+
const inboundMsgId = parsed.messageId;
|
|
2084
2137
|
helpers.waitUntil(
|
|
2085
|
-
client.addReaction({ messageId:
|
|
2086
|
-
|
|
2087
|
-
if (m) m.ackReactionId = reactionId;
|
|
2138
|
+
client.addReaction({ messageId: inboundMsgId, emojiType: emoji }).then(({ reactionId }) => {
|
|
2139
|
+
ackReactions.set(inboundMsgId, { reactionId, createdAt: Date.now() });
|
|
2088
2140
|
}).catch((e) => {
|
|
2089
2141
|
console.warn(
|
|
2090
2142
|
"[eve-lark] ack reaction failed:",
|
|
@@ -2114,7 +2166,7 @@ function createLarkChannel(optionsInput) {
|
|
|
2114
2166
|
helpers.waitUntil(
|
|
2115
2167
|
(async () => {
|
|
2116
2168
|
if (pending.cardMessageId && selectedOpt) {
|
|
2117
|
-
const ctrlForBuffer = controllers.get(pending.
|
|
2169
|
+
const ctrlForBuffer = controllers.get(pending.turnId);
|
|
2118
2170
|
const priorBuffer = ctrlForBuffer?.getBuffer() ?? void 0;
|
|
2119
2171
|
try {
|
|
2120
2172
|
await client.patchCard({
|
|
@@ -2180,9 +2232,16 @@ function createLarkChannel(optionsInput) {
|
|
|
2180
2232
|
const sessionId = ctx.session.id;
|
|
2181
2233
|
const info = sessionInfoFromCtx(ctx);
|
|
2182
2234
|
if (!info) return;
|
|
2235
|
+
const turnId = data?.turnId;
|
|
2236
|
+
if (!turnId) return;
|
|
2183
2237
|
const d = data;
|
|
2184
2238
|
if (typeof d.messageDelta !== "string") return;
|
|
2185
|
-
const ctrl = getController(
|
|
2239
|
+
const ctrl = getController(turnId, info);
|
|
2240
|
+
const source = turnSources.get(turnId);
|
|
2241
|
+
if (source) {
|
|
2242
|
+
console.log(`[eve-lark] quote-reply turnId=${turnId} sessionId=${sessionId} \u2192 REPLY ${source}`);
|
|
2243
|
+
ctrl.setReplyTarget(source);
|
|
2244
|
+
}
|
|
2186
2245
|
ctrl.appendDelta(d.messageDelta);
|
|
2187
2246
|
},
|
|
2188
2247
|
// Model is about to call tools. Record each call on the streaming
|
|
@@ -2194,13 +2253,14 @@ function createLarkChannel(optionsInput) {
|
|
|
2194
2253
|
// surface to update.
|
|
2195
2254
|
async "actions.requested"(data, _channel, ctx) {
|
|
2196
2255
|
if (options.replyMode !== "streaming" && options.replyMode !== "streaming-v2") return;
|
|
2197
|
-
const sessionId = ctx.session.id;
|
|
2198
2256
|
const info = sessionInfoFromCtx(ctx);
|
|
2199
2257
|
if (!info) return;
|
|
2258
|
+
const turnId = data?.turnId;
|
|
2259
|
+
if (!turnId) return;
|
|
2200
2260
|
const d = data;
|
|
2201
2261
|
const names = (d.actions ?? []).map((a) => a.toolName).filter((n) => typeof n === "string");
|
|
2202
2262
|
if (names.length === 0) return;
|
|
2203
|
-
const ctrl = getController(
|
|
2263
|
+
const ctrl = getController(turnId, info);
|
|
2204
2264
|
for (const name of names) {
|
|
2205
2265
|
ctrl.addToolCall(name);
|
|
2206
2266
|
}
|
|
@@ -2209,10 +2269,11 @@ function createLarkChannel(optionsInput) {
|
|
|
2209
2269
|
// the user keeps the tool history at the top of the card through end
|
|
2210
2270
|
// of turn. Best-effort: if we can't find the controller (e.g. post
|
|
2211
2271
|
// mode, or session already cleaned up), no-op.
|
|
2212
|
-
async "action.result"(data, _channel,
|
|
2272
|
+
async "action.result"(data, _channel, _ctx) {
|
|
2213
2273
|
if (options.replyMode !== "streaming" && options.replyMode !== "streaming-v2") return;
|
|
2214
|
-
const
|
|
2215
|
-
|
|
2274
|
+
const turnId = data?.turnId;
|
|
2275
|
+
if (!turnId) return;
|
|
2276
|
+
const ctrl = controllers.get(turnId);
|
|
2216
2277
|
if (!ctrl) return;
|
|
2217
2278
|
const d = data;
|
|
2218
2279
|
const name = d.result?.toolName;
|
|
@@ -2229,14 +2290,19 @@ function createLarkChannel(optionsInput) {
|
|
|
2229
2290
|
console.warn(`[eve-lark] input.requested: no session info (sessionId=${sessionId})`);
|
|
2230
2291
|
return;
|
|
2231
2292
|
}
|
|
2293
|
+
const turnId = data?.turnId;
|
|
2294
|
+
if (!turnId) {
|
|
2295
|
+
console.warn(`[eve-lark] input.requested: no turnId (sessionId=${sessionId})`);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2232
2298
|
const d = data;
|
|
2233
2299
|
const requests = d.requests ?? [];
|
|
2234
2300
|
if (requests.length === 0) return;
|
|
2235
2301
|
console.log(
|
|
2236
|
-
`[eve-lark] input.requested sessionId=${sessionId} chatId=${info.chatId} count=${requests.length}`
|
|
2302
|
+
`[eve-lark] input.requested sessionId=${sessionId} turnId=${turnId} chatId=${info.chatId} count=${requests.length}`
|
|
2237
2303
|
);
|
|
2238
2304
|
for (const req of requests) {
|
|
2239
|
-
const existingCtrl = controllers.get(
|
|
2305
|
+
const existingCtrl = controllers.get(turnId);
|
|
2240
2306
|
const canPatchExisting = existingCtrl && existingCtrl.getMessageId() && (options.replyMode === "streaming" || options.replyMode === "streaming-v2");
|
|
2241
2307
|
let cardMessageId;
|
|
2242
2308
|
if (canPatchExisting && existingCtrl) {
|
|
@@ -2263,6 +2329,7 @@ function createLarkChannel(optionsInput) {
|
|
|
2263
2329
|
const pending = {
|
|
2264
2330
|
requestId: req.requestId,
|
|
2265
2331
|
sessionId,
|
|
2332
|
+
turnId,
|
|
2266
2333
|
chatId: info.chatId,
|
|
2267
2334
|
rootId: info.rootId,
|
|
2268
2335
|
parentId: info.parentId,
|
|
@@ -2280,6 +2347,13 @@ function createLarkChannel(optionsInput) {
|
|
|
2280
2347
|
}
|
|
2281
2348
|
},
|
|
2282
2349
|
// Terminal — deliver the final reply, then clean up the ack reaction.
|
|
2350
|
+
// The per-turn controller is NOT destroyed here: in HITL flows eve fires
|
|
2351
|
+
// input.requested AFTER message.completed (still in the same turn), and
|
|
2352
|
+
// that handler needs the controller to patch the streaming card with the
|
|
2353
|
+
// ask UI inline. The controller is torn down by turn.completed (the true
|
|
2354
|
+
// end of turn). Duplicate message.completed events on the same turn are
|
|
2355
|
+
// deduped by the controller's finalize() state guard (state→completed on
|
|
2356
|
+
// first call, no-op thereafter).
|
|
2283
2357
|
async "message.completed"(data, _channel, ctx) {
|
|
2284
2358
|
const sessionId = ctx.session.id;
|
|
2285
2359
|
const info = sessionInfoFromCtx(ctx);
|
|
@@ -2289,14 +2363,28 @@ function createLarkChannel(optionsInput) {
|
|
|
2289
2363
|
}
|
|
2290
2364
|
const d = data;
|
|
2291
2365
|
const rawText = typeof d.message === "string" ? d.message : "";
|
|
2366
|
+
const mcTurnId = data?.turnId;
|
|
2292
2367
|
console.log(
|
|
2293
|
-
`[eve-lark] message.completed sessionId=${sessionId} chatId=${info.chatId} msgLen=${rawText.length}`
|
|
2368
|
+
`[eve-lark] message.completed sessionId=${sessionId} chatId=${info.chatId} msgLen=${rawText.length}` + (mcTurnId ? ` turnId=${mcTurnId}` : "")
|
|
2294
2369
|
);
|
|
2295
2370
|
const text = rawText.length > 0 ? rawText : EMPTY_REPLY_TEXT;
|
|
2371
|
+
const isStreaming = options.replyMode === "streaming" || options.replyMode === "streaming-v2";
|
|
2372
|
+
if (isStreaming && mcTurnId && !controllers.has(mcTurnId)) {
|
|
2373
|
+
if (!cleanedTurns.has(mcTurnId)) {
|
|
2374
|
+
cleanedTurns.add(mcTurnId);
|
|
2375
|
+
await cleanupAckForTurn(mcTurnId);
|
|
2376
|
+
}
|
|
2377
|
+
return;
|
|
2378
|
+
}
|
|
2296
2379
|
try {
|
|
2297
|
-
await deliverReply(
|
|
2380
|
+
await deliverReply(mcTurnId, info, text);
|
|
2298
2381
|
} finally {
|
|
2299
|
-
|
|
2382
|
+
if (mcTurnId && !cleanedTurns.has(mcTurnId)) {
|
|
2383
|
+
cleanedTurns.add(mcTurnId);
|
|
2384
|
+
await cleanupAckForTurn(mcTurnId);
|
|
2385
|
+
} else if (!mcTurnId) {
|
|
2386
|
+
await cleanupAckForSession(sessionId);
|
|
2387
|
+
}
|
|
2300
2388
|
}
|
|
2301
2389
|
},
|
|
2302
2390
|
async "turn.failed"(data, _channel, ctx) {
|
|
@@ -2310,33 +2398,39 @@ function createLarkChannel(optionsInput) {
|
|
|
2310
2398
|
console.warn(`[eve-lark] turn.failed: no session info (sessionId=${sessionId})`);
|
|
2311
2399
|
return;
|
|
2312
2400
|
}
|
|
2401
|
+
const failedTurnId = data?.turnId;
|
|
2313
2402
|
const userText = formatFailureMessage(data, "turn failed", { sentence: "turn" });
|
|
2314
2403
|
const errorId = extractErrorId(data?.details);
|
|
2315
2404
|
console.warn(
|
|
2316
|
-
`[eve-lark] turn.failed sessionId=${sessionId} chatId=${info.chatId} err="${userText.slice(0, 200)}"` + (errorId ? ` errorId=${errorId}` : "")
|
|
2405
|
+
`[eve-lark] turn.failed sessionId=${sessionId} chatId=${info.chatId} err="${userText.slice(0, 200)}"` + (failedTurnId ? ` turnId=${failedTurnId}` : "") + (errorId ? ` errorId=${errorId}` : "")
|
|
2317
2406
|
);
|
|
2318
|
-
const ctrl = controllers.get(
|
|
2407
|
+
const ctrl = failedTurnId ? controllers.get(failedTurnId) : void 0;
|
|
2319
2408
|
if (ctrl) {
|
|
2320
2409
|
try {
|
|
2321
2410
|
await ctrl.abort(userText);
|
|
2322
|
-
console.log(`[eve-lark] error shown via streaming abort (
|
|
2411
|
+
console.log(`[eve-lark] error shown via streaming abort (turnId=${failedTurnId})`);
|
|
2323
2412
|
} catch (e) {
|
|
2324
2413
|
console.warn(
|
|
2325
|
-
`[eve-lark] turn.failed: streaming abort failed, will deliver fresh error (
|
|
2414
|
+
`[eve-lark] turn.failed: streaming abort failed, will deliver fresh error (turnId=${failedTurnId}):`,
|
|
2326
2415
|
e instanceof Error ? e.message : e
|
|
2327
2416
|
);
|
|
2328
2417
|
try {
|
|
2329
|
-
await deliverReply(
|
|
2418
|
+
await deliverReply(failedTurnId, info, userText);
|
|
2330
2419
|
} catch {
|
|
2331
2420
|
}
|
|
2332
2421
|
}
|
|
2333
2422
|
} else {
|
|
2334
2423
|
try {
|
|
2335
|
-
await deliverReply(
|
|
2424
|
+
await deliverReply(failedTurnId, info, userText);
|
|
2336
2425
|
} catch {
|
|
2337
2426
|
}
|
|
2338
2427
|
}
|
|
2339
|
-
|
|
2428
|
+
if (!failedTurnId || !cleanedTurns.has(failedTurnId)) {
|
|
2429
|
+
if (failedTurnId) cleanedTurns.add(failedTurnId);
|
|
2430
|
+
if (failedTurnId) await cleanupAckForTurn(failedTurnId);
|
|
2431
|
+
else await cleanupAckForSession(sessionId);
|
|
2432
|
+
}
|
|
2433
|
+
if (failedTurnId) dropTurn(failedTurnId);
|
|
2340
2434
|
},
|
|
2341
2435
|
async "session.failed"(data) {
|
|
2342
2436
|
const userText = formatFailureMessage(data, "session failed", { sentence: "session" });
|
|
@@ -2345,36 +2439,44 @@ function createLarkChannel(optionsInput) {
|
|
|
2345
2439
|
`[eve-lark] session.failed: ${userText}` + (errorId ? ` (errorId=${errorId})` : "")
|
|
2346
2440
|
);
|
|
2347
2441
|
},
|
|
2348
|
-
// A new turn is starting
|
|
2349
|
-
//
|
|
2350
|
-
//
|
|
2351
|
-
//
|
|
2352
|
-
//
|
|
2353
|
-
//
|
|
2442
|
+
// A new turn is starting. Per-turn controllers mean we no longer reset
|
|
2443
|
+
// existing controllers here — each turn gets a fresh one (created lazily
|
|
2444
|
+
// in message.appended / actions.requested). The only side effect left is
|
|
2445
|
+
// mapping turnId → inbound source message: shift the head of the
|
|
2446
|
+
// session's FIFO (populated by the webhook handler) so message.appended
|
|
2447
|
+
// can aim this turn's card at the right user message and terminal events
|
|
2448
|
+
// can clean up the right ack reaction.
|
|
2354
2449
|
async "turn.started"(_data, _channel, ctx) {
|
|
2355
2450
|
const sessionId = ctx?.session?.id;
|
|
2356
2451
|
if (!sessionId) return;
|
|
2357
|
-
const
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2452
|
+
const tsTurnId = _data?.turnId;
|
|
2453
|
+
console.log(`[eve-lark] turn.started sessionId=${sessionId}` + (tsTurnId ? ` turnId=${tsTurnId}` : ""));
|
|
2454
|
+
if (!tsTurnId) return;
|
|
2455
|
+
const queue = currentInboundMsgIds.get(sessionId);
|
|
2456
|
+
if (queue && queue.length > 0) {
|
|
2457
|
+
const source = queue.shift();
|
|
2458
|
+
if (queue.length === 0) currentInboundMsgIds.delete(sessionId);
|
|
2459
|
+
turnSources.set(tsTurnId, source);
|
|
2365
2460
|
}
|
|
2366
2461
|
},
|
|
2367
2462
|
// Turn ended cleanly. eve fires this after the final message.completed
|
|
2368
2463
|
// (or instead of it when the assistant step ended in tool-calls with no
|
|
2369
|
-
// visible text).
|
|
2370
|
-
//
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2464
|
+
// visible text). Clean up the ack reaction and destroy the per-turn
|
|
2465
|
+
// controller (idempotent — message.completed/turn.failed may have done
|
|
2466
|
+
// it already).
|
|
2467
|
+
async "turn.completed"(data, _channel, _ctx) {
|
|
2468
|
+
const turnId = data?.turnId;
|
|
2469
|
+
if (!turnId) return;
|
|
2470
|
+
if (cleanedTurns.has(turnId)) {
|
|
2471
|
+
dropTurn(turnId);
|
|
2472
|
+
return;
|
|
2473
|
+
}
|
|
2474
|
+
cleanedTurns.add(turnId);
|
|
2374
2475
|
try {
|
|
2375
|
-
await
|
|
2476
|
+
await cleanupAckForTurn(turnId);
|
|
2376
2477
|
} catch {
|
|
2377
2478
|
}
|
|
2479
|
+
dropTurn(turnId);
|
|
2378
2480
|
},
|
|
2379
2481
|
// The agent needs the user to sign in to an external service (e.g.
|
|
2380
2482
|
// GitHub, Slack, Linear). Render a card with a "Sign in with <X>"
|