eve-lark 0.4.6 → 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 +195 -98
- 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,15 +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 expectFreshCard = /* @__PURE__ */ new Set();
|
|
1720
1751
|
const ackReactions = /* @__PURE__ */ new Map();
|
|
1721
|
-
const
|
|
1722
|
-
|
|
1723
|
-
|
|
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);
|
|
1724
1757
|
if (!ctrl) {
|
|
1725
1758
|
ctrl = new StreamingCardController(client, {
|
|
1726
1759
|
chatId: meta.chatId,
|
|
@@ -1730,21 +1763,18 @@ function createLarkChannel(optionsInput) {
|
|
|
1730
1763
|
createThresholdMs: options.streamCreateThresholdMs,
|
|
1731
1764
|
useCardKitV2: options.replyMode === "streaming-v2"
|
|
1732
1765
|
});
|
|
1733
|
-
controllers.set(
|
|
1734
|
-
}
|
|
1735
|
-
if (sessionMeta.has(sessionId)) {
|
|
1736
|
-
sessionMeta.get(sessionId).touchedAt = Date.now();
|
|
1737
|
-
} else {
|
|
1738
|
-
sessionMeta.set(sessionId, {
|
|
1739
|
-
chatId: meta.chatId,
|
|
1740
|
-
rootId: meta.rootId,
|
|
1741
|
-
parentId: meta.parentId,
|
|
1742
|
-
touchedAt: Date.now()
|
|
1743
|
-
});
|
|
1766
|
+
controllers.set(turnId, ctrl);
|
|
1744
1767
|
}
|
|
1768
|
+
turnTouchedAt.set(turnId, Date.now());
|
|
1745
1769
|
return ctrl;
|
|
1746
1770
|
}
|
|
1747
1771
|
__name(getController, "getController");
|
|
1772
|
+
function dropTurn(turnId) {
|
|
1773
|
+
controllers.delete(turnId);
|
|
1774
|
+
turnSources.delete(turnId);
|
|
1775
|
+
turnTouchedAt.delete(turnId);
|
|
1776
|
+
}
|
|
1777
|
+
__name(dropTurn, "dropTurn");
|
|
1748
1778
|
async function cleanupAckReaction(messageId) {
|
|
1749
1779
|
const ack = ackReactions.get(messageId);
|
|
1750
1780
|
if (!ack) return;
|
|
@@ -1760,13 +1790,21 @@ function createLarkChannel(optionsInput) {
|
|
|
1760
1790
|
}
|
|
1761
1791
|
__name(cleanupAckReaction, "cleanupAckReaction");
|
|
1762
1792
|
async function cleanupAckForSession(sessionId) {
|
|
1763
|
-
const
|
|
1793
|
+
const queue = currentInboundMsgIds.get(sessionId);
|
|
1794
|
+
if (!queue || queue.length === 0) return;
|
|
1795
|
+
const msgId = queue.shift();
|
|
1764
1796
|
if (!msgId) return;
|
|
1765
|
-
|
|
1797
|
+
if (queue.length === 0) currentInboundMsgIds.delete(sessionId);
|
|
1766
1798
|
await cleanupAckReaction(msgId);
|
|
1767
1799
|
}
|
|
1768
1800
|
__name(cleanupAckForSession, "cleanupAckForSession");
|
|
1769
|
-
async function
|
|
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) {
|
|
1770
1808
|
if (options.replyMode === "post") {
|
|
1771
1809
|
try {
|
|
1772
1810
|
await client.sendPost({
|
|
@@ -1775,11 +1813,11 @@ function createLarkChannel(optionsInput) {
|
|
|
1775
1813
|
rootId: info.rootId,
|
|
1776
1814
|
parentId: info.parentId
|
|
1777
1815
|
});
|
|
1778
|
-
console.log(`[eve-lark] delivered via sendPost (
|
|
1816
|
+
console.log(`[eve-lark] delivered via sendPost (turnId=${turnId ?? "?"})`);
|
|
1779
1817
|
return;
|
|
1780
1818
|
} catch (postErr) {
|
|
1781
1819
|
console.warn(
|
|
1782
|
-
`[eve-lark] sendPost failed; falling back to plain text (
|
|
1820
|
+
`[eve-lark] sendPost failed; falling back to plain text (turnId=${turnId ?? "?"}):`,
|
|
1783
1821
|
postErr instanceof Error ? postErr.message : postErr
|
|
1784
1822
|
);
|
|
1785
1823
|
}
|
|
@@ -1790,40 +1828,43 @@ function createLarkChannel(optionsInput) {
|
|
|
1790
1828
|
rootId: info.rootId,
|
|
1791
1829
|
parentId: info.parentId
|
|
1792
1830
|
});
|
|
1793
|
-
console.log(`[eve-lark] delivered via sendText fallback (
|
|
1831
|
+
console.log(`[eve-lark] delivered via sendText fallback (turnId=${turnId ?? "?"})`);
|
|
1794
1832
|
} catch (textErr) {
|
|
1795
1833
|
console.error(
|
|
1796
|
-
`[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 ?? "?"}):`,
|
|
1797
1835
|
textErr instanceof Error ? textErr.message : textErr
|
|
1798
1836
|
);
|
|
1799
1837
|
}
|
|
1800
1838
|
return;
|
|
1801
1839
|
}
|
|
1802
1840
|
if (options.replyMode === "streaming" || options.replyMode === "streaming-v2") {
|
|
1803
|
-
const ctrl = controllers.get(
|
|
1804
|
-
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
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
|
+
}
|
|
1813
1853
|
}
|
|
1814
1854
|
}
|
|
1815
1855
|
try {
|
|
1816
|
-
await client.sendCard({
|
|
1856
|
+
const res = await client.sendCard({
|
|
1817
1857
|
chatId: info.chatId,
|
|
1818
1858
|
card: buildTextCard(text),
|
|
1819
1859
|
rootId: info.rootId,
|
|
1820
1860
|
parentId: info.parentId
|
|
1821
1861
|
});
|
|
1822
|
-
console.log(`[eve-lark] delivered via sendCard (
|
|
1862
|
+
console.log(`[eve-lark] delivered via sendCard (turnId=${turnId ?? "?"})`);
|
|
1863
|
+
void res;
|
|
1823
1864
|
return;
|
|
1824
1865
|
} catch (cardErr) {
|
|
1825
1866
|
console.warn(
|
|
1826
|
-
`[eve-lark] sendCard failed; falling back to plain text (
|
|
1867
|
+
`[eve-lark] sendCard failed; falling back to plain text (turnId=${turnId ?? "?"}):`,
|
|
1827
1868
|
cardErr instanceof Error ? cardErr.message : cardErr
|
|
1828
1869
|
);
|
|
1829
1870
|
}
|
|
@@ -1834,10 +1875,10 @@ function createLarkChannel(optionsInput) {
|
|
|
1834
1875
|
rootId: info.rootId,
|
|
1835
1876
|
parentId: info.parentId
|
|
1836
1877
|
});
|
|
1837
|
-
console.log(`[eve-lark] delivered via sendText fallback (
|
|
1878
|
+
console.log(`[eve-lark] delivered via sendText fallback (turnId=${turnId ?? "?"})`);
|
|
1838
1879
|
} catch (textErr) {
|
|
1839
1880
|
console.error(
|
|
1840
|
-
`[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 ?? "?"}):`,
|
|
1841
1882
|
textErr instanceof Error ? textErr.message : textErr
|
|
1842
1883
|
);
|
|
1843
1884
|
}
|
|
@@ -1849,9 +1890,13 @@ function createLarkChannel(optionsInput) {
|
|
|
1849
1890
|
if (now - lastSweepAt < SWEEP_INTERVAL_MS) return;
|
|
1850
1891
|
lastSweepAt = now;
|
|
1851
1892
|
const cutoff = now - STALE_SESSION_MS;
|
|
1893
|
+
for (const [turnId, touchedAt] of turnTouchedAt) {
|
|
1894
|
+
if (touchedAt < cutoff) {
|
|
1895
|
+
dropTurn(turnId);
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1852
1898
|
for (const [id, meta] of sessionMeta) {
|
|
1853
1899
|
if (meta.touchedAt < cutoff) {
|
|
1854
|
-
controllers.delete(id);
|
|
1855
1900
|
sessionMeta.delete(id);
|
|
1856
1901
|
}
|
|
1857
1902
|
}
|
|
@@ -2076,8 +2121,10 @@ function createLarkChannel(optionsInput) {
|
|
|
2076
2121
|
console.log(
|
|
2077
2122
|
`[eve-lark] helpers.send returned sessionId=${session.id} for chatId=${parsed.chatId}`
|
|
2078
2123
|
);
|
|
2079
|
-
|
|
2080
|
-
|
|
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);
|
|
2081
2128
|
sessionMeta.set(session.id, {
|
|
2082
2129
|
chatId: parsed.chatId,
|
|
2083
2130
|
rootId: parsed.rootId ?? void 0,
|
|
@@ -2119,7 +2166,7 @@ function createLarkChannel(optionsInput) {
|
|
|
2119
2166
|
helpers.waitUntil(
|
|
2120
2167
|
(async () => {
|
|
2121
2168
|
if (pending.cardMessageId && selectedOpt) {
|
|
2122
|
-
const ctrlForBuffer = controllers.get(pending.
|
|
2169
|
+
const ctrlForBuffer = controllers.get(pending.turnId);
|
|
2123
2170
|
const priorBuffer = ctrlForBuffer?.getBuffer() ?? void 0;
|
|
2124
2171
|
try {
|
|
2125
2172
|
await client.patchCard({
|
|
@@ -2185,9 +2232,16 @@ function createLarkChannel(optionsInput) {
|
|
|
2185
2232
|
const sessionId = ctx.session.id;
|
|
2186
2233
|
const info = sessionInfoFromCtx(ctx);
|
|
2187
2234
|
if (!info) return;
|
|
2235
|
+
const turnId = data?.turnId;
|
|
2236
|
+
if (!turnId) return;
|
|
2188
2237
|
const d = data;
|
|
2189
2238
|
if (typeof d.messageDelta !== "string") return;
|
|
2190
|
-
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
|
+
}
|
|
2191
2245
|
ctrl.appendDelta(d.messageDelta);
|
|
2192
2246
|
},
|
|
2193
2247
|
// Model is about to call tools. Record each call on the streaming
|
|
@@ -2199,13 +2253,14 @@ function createLarkChannel(optionsInput) {
|
|
|
2199
2253
|
// surface to update.
|
|
2200
2254
|
async "actions.requested"(data, _channel, ctx) {
|
|
2201
2255
|
if (options.replyMode !== "streaming" && options.replyMode !== "streaming-v2") return;
|
|
2202
|
-
const sessionId = ctx.session.id;
|
|
2203
2256
|
const info = sessionInfoFromCtx(ctx);
|
|
2204
2257
|
if (!info) return;
|
|
2258
|
+
const turnId = data?.turnId;
|
|
2259
|
+
if (!turnId) return;
|
|
2205
2260
|
const d = data;
|
|
2206
2261
|
const names = (d.actions ?? []).map((a) => a.toolName).filter((n) => typeof n === "string");
|
|
2207
2262
|
if (names.length === 0) return;
|
|
2208
|
-
const ctrl = getController(
|
|
2263
|
+
const ctrl = getController(turnId, info);
|
|
2209
2264
|
for (const name of names) {
|
|
2210
2265
|
ctrl.addToolCall(name);
|
|
2211
2266
|
}
|
|
@@ -2214,10 +2269,11 @@ function createLarkChannel(optionsInput) {
|
|
|
2214
2269
|
// the user keeps the tool history at the top of the card through end
|
|
2215
2270
|
// of turn. Best-effort: if we can't find the controller (e.g. post
|
|
2216
2271
|
// mode, or session already cleaned up), no-op.
|
|
2217
|
-
async "action.result"(data, _channel,
|
|
2272
|
+
async "action.result"(data, _channel, _ctx) {
|
|
2218
2273
|
if (options.replyMode !== "streaming" && options.replyMode !== "streaming-v2") return;
|
|
2219
|
-
const
|
|
2220
|
-
|
|
2274
|
+
const turnId = data?.turnId;
|
|
2275
|
+
if (!turnId) return;
|
|
2276
|
+
const ctrl = controllers.get(turnId);
|
|
2221
2277
|
if (!ctrl) return;
|
|
2222
2278
|
const d = data;
|
|
2223
2279
|
const name = d.result?.toolName;
|
|
@@ -2234,14 +2290,19 @@ function createLarkChannel(optionsInput) {
|
|
|
2234
2290
|
console.warn(`[eve-lark] input.requested: no session info (sessionId=${sessionId})`);
|
|
2235
2291
|
return;
|
|
2236
2292
|
}
|
|
2293
|
+
const turnId = data?.turnId;
|
|
2294
|
+
if (!turnId) {
|
|
2295
|
+
console.warn(`[eve-lark] input.requested: no turnId (sessionId=${sessionId})`);
|
|
2296
|
+
return;
|
|
2297
|
+
}
|
|
2237
2298
|
const d = data;
|
|
2238
2299
|
const requests = d.requests ?? [];
|
|
2239
2300
|
if (requests.length === 0) return;
|
|
2240
2301
|
console.log(
|
|
2241
|
-
`[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}`
|
|
2242
2303
|
);
|
|
2243
2304
|
for (const req of requests) {
|
|
2244
|
-
const existingCtrl = controllers.get(
|
|
2305
|
+
const existingCtrl = controllers.get(turnId);
|
|
2245
2306
|
const canPatchExisting = existingCtrl && existingCtrl.getMessageId() && (options.replyMode === "streaming" || options.replyMode === "streaming-v2");
|
|
2246
2307
|
let cardMessageId;
|
|
2247
2308
|
if (canPatchExisting && existingCtrl) {
|
|
@@ -2268,6 +2329,7 @@ function createLarkChannel(optionsInput) {
|
|
|
2268
2329
|
const pending = {
|
|
2269
2330
|
requestId: req.requestId,
|
|
2270
2331
|
sessionId,
|
|
2332
|
+
turnId,
|
|
2271
2333
|
chatId: info.chatId,
|
|
2272
2334
|
rootId: info.rootId,
|
|
2273
2335
|
parentId: info.parentId,
|
|
@@ -2285,6 +2347,13 @@ function createLarkChannel(optionsInput) {
|
|
|
2285
2347
|
}
|
|
2286
2348
|
},
|
|
2287
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).
|
|
2288
2357
|
async "message.completed"(data, _channel, ctx) {
|
|
2289
2358
|
const sessionId = ctx.session.id;
|
|
2290
2359
|
const info = sessionInfoFromCtx(ctx);
|
|
@@ -2294,14 +2363,28 @@ function createLarkChannel(optionsInput) {
|
|
|
2294
2363
|
}
|
|
2295
2364
|
const d = data;
|
|
2296
2365
|
const rawText = typeof d.message === "string" ? d.message : "";
|
|
2366
|
+
const mcTurnId = data?.turnId;
|
|
2297
2367
|
console.log(
|
|
2298
|
-
`[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}` : "")
|
|
2299
2369
|
);
|
|
2300
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
|
+
}
|
|
2301
2379
|
try {
|
|
2302
|
-
await deliverReply(
|
|
2380
|
+
await deliverReply(mcTurnId, info, text);
|
|
2303
2381
|
} finally {
|
|
2304
|
-
|
|
2382
|
+
if (mcTurnId && !cleanedTurns.has(mcTurnId)) {
|
|
2383
|
+
cleanedTurns.add(mcTurnId);
|
|
2384
|
+
await cleanupAckForTurn(mcTurnId);
|
|
2385
|
+
} else if (!mcTurnId) {
|
|
2386
|
+
await cleanupAckForSession(sessionId);
|
|
2387
|
+
}
|
|
2305
2388
|
}
|
|
2306
2389
|
},
|
|
2307
2390
|
async "turn.failed"(data, _channel, ctx) {
|
|
@@ -2315,33 +2398,39 @@ function createLarkChannel(optionsInput) {
|
|
|
2315
2398
|
console.warn(`[eve-lark] turn.failed: no session info (sessionId=${sessionId})`);
|
|
2316
2399
|
return;
|
|
2317
2400
|
}
|
|
2401
|
+
const failedTurnId = data?.turnId;
|
|
2318
2402
|
const userText = formatFailureMessage(data, "turn failed", { sentence: "turn" });
|
|
2319
2403
|
const errorId = extractErrorId(data?.details);
|
|
2320
2404
|
console.warn(
|
|
2321
|
-
`[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}` : "")
|
|
2322
2406
|
);
|
|
2323
|
-
const ctrl = controllers.get(
|
|
2407
|
+
const ctrl = failedTurnId ? controllers.get(failedTurnId) : void 0;
|
|
2324
2408
|
if (ctrl) {
|
|
2325
2409
|
try {
|
|
2326
2410
|
await ctrl.abort(userText);
|
|
2327
|
-
console.log(`[eve-lark] error shown via streaming abort (
|
|
2411
|
+
console.log(`[eve-lark] error shown via streaming abort (turnId=${failedTurnId})`);
|
|
2328
2412
|
} catch (e) {
|
|
2329
2413
|
console.warn(
|
|
2330
|
-
`[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}):`,
|
|
2331
2415
|
e instanceof Error ? e.message : e
|
|
2332
2416
|
);
|
|
2333
2417
|
try {
|
|
2334
|
-
await deliverReply(
|
|
2418
|
+
await deliverReply(failedTurnId, info, userText);
|
|
2335
2419
|
} catch {
|
|
2336
2420
|
}
|
|
2337
2421
|
}
|
|
2338
2422
|
} else {
|
|
2339
2423
|
try {
|
|
2340
|
-
await deliverReply(
|
|
2424
|
+
await deliverReply(failedTurnId, info, userText);
|
|
2341
2425
|
} catch {
|
|
2342
2426
|
}
|
|
2343
2427
|
}
|
|
2344
|
-
|
|
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);
|
|
2345
2434
|
},
|
|
2346
2435
|
async "session.failed"(data) {
|
|
2347
2436
|
const userText = formatFailureMessage(data, "session failed", { sentence: "session" });
|
|
@@ -2350,36 +2439,44 @@ function createLarkChannel(optionsInput) {
|
|
|
2350
2439
|
`[eve-lark] session.failed: ${userText}` + (errorId ? ` (errorId=${errorId})` : "")
|
|
2351
2440
|
);
|
|
2352
2441
|
},
|
|
2353
|
-
// A new turn is starting
|
|
2354
|
-
//
|
|
2355
|
-
//
|
|
2356
|
-
//
|
|
2357
|
-
//
|
|
2358
|
-
//
|
|
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.
|
|
2359
2449
|
async "turn.started"(_data, _channel, ctx) {
|
|
2360
2450
|
const sessionId = ctx?.session?.id;
|
|
2361
2451
|
if (!sessionId) return;
|
|
2362
|
-
const
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
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);
|
|
2370
2460
|
}
|
|
2371
2461
|
},
|
|
2372
2462
|
// Turn ended cleanly. eve fires this after the final message.completed
|
|
2373
2463
|
// (or instead of it when the assistant step ended in tool-calls with no
|
|
2374
|
-
// visible text).
|
|
2375
|
-
//
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
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);
|
|
2379
2475
|
try {
|
|
2380
|
-
await
|
|
2476
|
+
await cleanupAckForTurn(turnId);
|
|
2381
2477
|
} catch {
|
|
2382
2478
|
}
|
|
2479
|
+
dropTurn(turnId);
|
|
2383
2480
|
},
|
|
2384
2481
|
// The agent needs the user to sign in to an external service (e.g.
|
|
2385
2482
|
// GitHub, Slack, Linear). Render a card with a "Sign in with <X>"
|