openclaw-lark-multi-agent 1.2.2 → 1.2.4
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/feishu-bot.d.ts +19 -11
- package/dist/feishu-bot.js +56 -59
- package/package.json +1 -1
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -21,6 +21,9 @@ export declare class FeishuBot {
|
|
|
21
21
|
private initializedSessions;
|
|
22
22
|
/** Per-chat busy lock: timestamp when became busy (0 = not busy) */
|
|
23
23
|
private busyChats;
|
|
24
|
+
/** Per-chat "stop" generation counter. Bumped by /stop so any in-flight
|
|
25
|
+
* auto-retry loop can detect it was cancelled and stop retrying immediately. */
|
|
26
|
+
private stopEpoch;
|
|
24
27
|
/** Per-chat pending reply message IDs (to ack with DONE when their trigger is processed) */
|
|
25
28
|
private pendingAckMessages;
|
|
26
29
|
/** Per-chat pending tool message sends (to await before final reply) */
|
|
@@ -134,19 +137,24 @@ export declare class FeishuBot {
|
|
|
134
137
|
* only on cheap text shape here, never on stopReason (the upstream rarely
|
|
135
138
|
* provides one for normal-looking-but-truncated finals).
|
|
136
139
|
*/
|
|
137
|
-
private looksTruncated;
|
|
138
140
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
141
|
+
* Heuristic: does this reply look genuinely truncated / cut off mid-task?
|
|
142
|
+
*
|
|
143
|
+
* IMPORTANT (per Stephen): only retry when there is CLEAR evidence the agent
|
|
144
|
+
* did not finish — do NOT retry just because a reply lacks a trailing period.
|
|
145
|
+
* Many complete replies legitimately end without sentence punctuation (a terse
|
|
146
|
+
* “好的”, a bullet list, a path, a number, a closing word). Treating those as
|
|
147
|
+
* truncated caused needless extra probe rounds, which hurts UX. So we gate on
|
|
148
|
+
* strong structural signals only:
|
|
149
|
+
* 1. Unbalanced code fence — almost certainly cut off mid code block.
|
|
150
|
+
* 2. Ends with a dangling connector / lead-in (“然后”, “接下来”, “let me”…)
|
|
151
|
+
* or a trailing clause-joining punctuation (comma/colon/semicolon),
|
|
152
|
+
* which means a sentence was left hanging.
|
|
153
|
+
* 3. Ends mid-word inside a Latin word right after a connector-less clause
|
|
154
|
+
* (very rare; only when the last “word” is unusually long), kept minimal.
|
|
155
|
+
* A plain “no sentence-ending punctuation” is deliberately NOT sufficient.
|
|
148
156
|
*/
|
|
149
|
-
private
|
|
157
|
+
private looksTruncated;
|
|
150
158
|
/** Current context usage percent for a session (totalTokens / contextTokens),
|
|
151
159
|
* or 0 if it cannot be determined. Same basis as /status. */
|
|
152
160
|
private getContextUsagePercent;
|
package/dist/feishu-bot.js
CHANGED
|
@@ -29,14 +29,6 @@ function autoRetryMax() { return Number(process.env.OPENCLAW_LARK_MULTI_AGENT_AU
|
|
|
29
29
|
// for the locale-appropriate phrase; detection accepts BOTH so either works.
|
|
30
30
|
const AUTO_RETRY_DONE_PHRASE = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_DONE_PHRASE || "\u7ed3\u675f\u4e86";
|
|
31
31
|
const AUTO_RETRY_DONE_PHRASE_EN = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_DONE_PHRASE_EN || "DONE";
|
|
32
|
-
// When the previous reply ended with a dangling tool-call tag (the model typed
|
|
33
|
-
// an invoke/tool call as plain text and it never ran), the auto-retry probe
|
|
34
|
-
// explicitly tells the agent its invoke format was broken and to retry, instead
|
|
35
|
-
// of the generic done-check. It still accepts the done phrase if actually done.
|
|
36
|
-
const AUTO_RETRY_BROKEN_TOOL_PROBE = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_BROKEN_TOOL_PROBE
|
|
37
|
-
|| `刚才的任务是不是没结束,invoke 格式错了,invoke 格式错了,请重试。如果结束了,回复“${AUTO_RETRY_DONE_PHRASE}”。`;
|
|
38
|
-
const AUTO_RETRY_BROKEN_TOOL_PROBE_EN = process.env.OPENCLAW_LARK_MULTI_AGENT_AUTO_RETRY_BROKEN_TOOL_PROBE_EN
|
|
39
|
-
|| `Your last task did not finish — the invoke format was broken, the invoke format was broken, please retry. If it is actually done, reply "${AUTO_RETRY_DONE_PHRASE_EN}".`;
|
|
40
32
|
// When an auto-retry probe errors AND context usage is at/above this percent,
|
|
41
33
|
// auto-compact the session once and keep retrying (instead of giving up). This
|
|
42
34
|
// rescues the “session too heavy → run times out / lock contention” failure mode.
|
|
@@ -75,6 +67,9 @@ export class FeishuBot {
|
|
|
75
67
|
initializedSessions = new Set();
|
|
76
68
|
/** Per-chat busy lock: timestamp when became busy (0 = not busy) */
|
|
77
69
|
busyChats = new Map();
|
|
70
|
+
/** Per-chat "stop" generation counter. Bumped by /stop so any in-flight
|
|
71
|
+
* auto-retry loop can detect it was cancelled and stop retrying immediately. */
|
|
72
|
+
stopEpoch = new Map();
|
|
78
73
|
/** Per-chat pending reply message IDs (to ack with DONE when their trigger is processed) */
|
|
79
74
|
pendingAckMessages = new Map();
|
|
80
75
|
/** Per-chat pending tool message sends (to await before final reply) */
|
|
@@ -1697,43 +1692,38 @@ export class FeishuBot {
|
|
|
1697
1692
|
* only on cheap text shape here, never on stopReason (the upstream rarely
|
|
1698
1693
|
* provides one for normal-looking-but-truncated finals).
|
|
1699
1694
|
*/
|
|
1695
|
+
/**
|
|
1696
|
+
* Heuristic: does this reply look genuinely truncated / cut off mid-task?
|
|
1697
|
+
*
|
|
1698
|
+
* IMPORTANT (per Stephen): only retry when there is CLEAR evidence the agent
|
|
1699
|
+
* did not finish — do NOT retry just because a reply lacks a trailing period.
|
|
1700
|
+
* Many complete replies legitimately end without sentence punctuation (a terse
|
|
1701
|
+
* “好的”, a bullet list, a path, a number, a closing word). Treating those as
|
|
1702
|
+
* truncated caused needless extra probe rounds, which hurts UX. So we gate on
|
|
1703
|
+
* strong structural signals only:
|
|
1704
|
+
* 1. Unbalanced code fence — almost certainly cut off mid code block.
|
|
1705
|
+
* 2. Ends with a dangling connector / lead-in (“然后”, “接下来”, “let me”…)
|
|
1706
|
+
* or a trailing clause-joining punctuation (comma/colon/semicolon),
|
|
1707
|
+
* which means a sentence was left hanging.
|
|
1708
|
+
* 3. Ends mid-word inside a Latin word right after a connector-less clause
|
|
1709
|
+
* (very rare; only when the last “word” is unusually long), kept minimal.
|
|
1710
|
+
* A plain “no sentence-ending punctuation” is deliberately NOT sufficient.
|
|
1711
|
+
*/
|
|
1700
1712
|
looksTruncated(text) {
|
|
1701
1713
|
const t = (text || "").trim();
|
|
1702
1714
|
if (!t)
|
|
1703
1715
|
return false; // empty/NO_REPLY handled elsewhere
|
|
1704
|
-
// Unbalanced code fence => almost certainly cut off mid-block.
|
|
1716
|
+
// (1) Unbalanced code fence => almost certainly cut off mid-block.
|
|
1705
1717
|
const fences = (t.match(/```/g) || []).length;
|
|
1706
1718
|
if (fences % 2 === 1)
|
|
1707
1719
|
return true;
|
|
1708
|
-
//
|
|
1720
|
+
// (2a) Trailing clause-joining punctuation: a sentence was left hanging.
|
|
1709
1721
|
if (/[,,::、;;]\s*$/u.test(t))
|
|
1710
1722
|
return true;
|
|
1711
|
-
|
|
1723
|
+
// (2b) Ends on an explicit continuation cue / lead-in.
|
|
1724
|
+
if (/(接下来|然后|首先|现在我|让我|下一步|我先|我来|我接着|接着|稍等|let me|now i|next|first|\b(?:and|then|so|to|the|a)\b)\s*$/iu.test(t))
|
|
1712
1725
|
return true;
|
|
1713
|
-
|
|
1714
|
-
// (“连句号都没有就感觉没成功”). Accept terminators across languages,
|
|
1715
|
-
// closing brackets/quotes, code fences, and common emoji endings.
|
|
1716
|
-
const endOk = /[。!?.!?…)\]」』】)"'`”’~~—✨✅👍🙏]\s*$/u.test(t)
|
|
1717
|
-
|| /```\s*$/.test(t)
|
|
1718
|
-
|| /[}\]]\s*$/.test(t);
|
|
1719
|
-
return !endOk;
|
|
1720
|
-
}
|
|
1721
|
-
/**
|
|
1722
|
-
* Did the model type a tool call as plain text instead of emitting a real
|
|
1723
|
-
* function call? When that happens the visible reply ends with a dangling
|
|
1724
|
-
* invoke / parameter / function_calls tag (with or without the `antml:`
|
|
1725
|
-
* prefix), because the tool-call markup leaked into the text channel and the
|
|
1726
|
-
* tool never actually ran. We detect such a tag anchored at the END of the
|
|
1727
|
-
* message — that dangling tag is the tell-tale Stephen flagged ("如果监测到
|
|
1728
|
-
* 这个结尾,说明 tool call 格式出了问题"). On detection the auto-retry loop
|
|
1729
|
-
* sends an explicit "invoke 格式错了,请重试" probe so the agent re-issues the
|
|
1730
|
-
* tool call correctly rather than the generic done-check.
|
|
1731
|
-
*/
|
|
1732
|
-
looksLikeBrokenToolCall(text) {
|
|
1733
|
-
const t = (text || "").trim();
|
|
1734
|
-
if (!t)
|
|
1735
|
-
return false;
|
|
1736
|
-
return /<\/?\s*(?:antml:)?(?:invoke|parameter|function_calls)\b[^>]*>\s*$/i.test(t);
|
|
1726
|
+
return false;
|
|
1737
1727
|
}
|
|
1738
1728
|
/** Current context usage percent for a session (totalTokens / contextTokens),
|
|
1739
1729
|
* or 0 if it cannot be determined. Same basis as /status. */
|
|
@@ -1833,12 +1823,19 @@ export class FeishuBot {
|
|
|
1833
1823
|
const maxRetry = autoRetryMax();
|
|
1834
1824
|
let compacted = false; // auto-compact at most once per auto-retry round
|
|
1835
1825
|
let probedAtLeastOnce = false;
|
|
1836
|
-
//
|
|
1837
|
-
//
|
|
1838
|
-
//
|
|
1839
|
-
|
|
1826
|
+
// Snapshot the stop generation at entry. If the user runs /stop while this
|
|
1827
|
+
// loop is in flight, handleStopCommand bumps the epoch; we detect the change
|
|
1828
|
+
// and stop retrying immediately instead of issuing more probes.
|
|
1829
|
+
const startStopEpoch = this.stopEpoch.get(chatId) || 0;
|
|
1830
|
+
const wasStopped = () => (this.stopEpoch.get(chatId) || 0) !== startStopEpoch;
|
|
1840
1831
|
for (let attempt = 1; attempt <= maxRetry; attempt++) {
|
|
1841
|
-
|
|
1832
|
+
// /stop while we were looping: abandon retries and deliver what we have.
|
|
1833
|
+
if (wasStopped()) {
|
|
1834
|
+
console.log(`[${this.config.name}] auto-retry: aborted by /stop for ${chatId.slice(-8)}`);
|
|
1835
|
+
if (!reply)
|
|
1836
|
+
throw new Error(`Agent error: ${params.initialError || "stopped by user"}`);
|
|
1837
|
+
return reply;
|
|
1838
|
+
}
|
|
1842
1839
|
if (!forceProbe) {
|
|
1843
1840
|
const parsed = this.extractBridgeAttachments(reply);
|
|
1844
1841
|
const trimmed = parsed.text.trim();
|
|
@@ -1851,32 +1848,20 @@ export class FeishuBot {
|
|
|
1851
1848
|
// and runtime failures have their own handling upstream.
|
|
1852
1849
|
if (!trimmed || trimmed.toUpperCase() === "NO_REPLY" || this.isRuntimeFailureText(trimmed))
|
|
1853
1850
|
return reply;
|
|
1854
|
-
|
|
1855
|
-
// signal — engage even if the generic truncation heuristic would miss it.
|
|
1856
|
-
brokenToolCall = this.looksLikeBrokenToolCall(trimmed);
|
|
1857
|
-
if (!this.looksTruncated(trimmed) && !brokenToolCall)
|
|
1851
|
+
if (!this.looksTruncated(trimmed))
|
|
1858
1852
|
return reply;
|
|
1859
1853
|
}
|
|
1860
1854
|
forceProbe = false; // consumed; subsequent rounds inspect the probe reply
|
|
1861
1855
|
const en = this.isEn(chatId);
|
|
1862
1856
|
await liveStatus?.progress(en
|
|
1863
|
-
? (
|
|
1864
|
-
|
|
1865
|
-
: `⚠️ Reply looks incomplete — auto-checking (${attempt}/${maxRetry})…`)
|
|
1866
|
-
: (brokenToolCall
|
|
1867
|
-
? `⚠️ 工具调用格式疑似出错,正在让它重试(${attempt}/${maxRetry})…`
|
|
1868
|
-
: `⚠️ 疑似任务未完成,正在自动重试(${attempt}/${maxRetry})…`)).catch(() => { });
|
|
1857
|
+
? `⚠️ Reply looks incomplete — auto-checking (${attempt}/${maxRetry})…`
|
|
1858
|
+
: `⚠️ 疑似任务未完成,正在自动重试(${attempt}/${maxRetry})…`).catch(() => { });
|
|
1869
1859
|
// One short line, in the user's own voice (we intentionally do not change
|
|
1870
1860
|
// senderName). Each locale uses its own done-phrase so the English probe
|
|
1871
|
-
// stays fully English; detection accepts both phrases.
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
const probe = brokenToolCall
|
|
1876
|
-
? (en ? AUTO_RETRY_BROKEN_TOOL_PROBE_EN : AUTO_RETRY_BROKEN_TOOL_PROBE)
|
|
1877
|
-
: (en
|
|
1878
|
-
? `Is that done? If so just reply "${AUTO_RETRY_DONE_PHRASE_EN}", otherwise keep going.`
|
|
1879
|
-
: `刚才那个任务结束了吗?结束了就回我“${AUTO_RETRY_DONE_PHRASE}”,没结束就接着做。`);
|
|
1861
|
+
// stays fully English; detection accepts both phrases.
|
|
1862
|
+
const probe = en
|
|
1863
|
+
? `Is that done? If so just reply "${AUTO_RETRY_DONE_PHRASE_EN}", otherwise keep going.`
|
|
1864
|
+
: `刚才那个任务结束了吗?结束了就回我“${AUTO_RETRY_DONE_PHRASE}”,没结束就接着做。`;
|
|
1880
1865
|
let probeReply;
|
|
1881
1866
|
try {
|
|
1882
1867
|
probeReply = await this.withSessionHealthMonitor(chatId, lastHumanMessageId, triggerId, this.openclawClient.chatSendWithContext({
|
|
@@ -1928,6 +1913,15 @@ export class FeishuBot {
|
|
|
1928
1913
|
return reply;
|
|
1929
1914
|
}
|
|
1930
1915
|
const probeText = this.extractBridgeAttachments(probeReply).text.trim();
|
|
1916
|
+
// /stop landed while the probe was in flight: stop here rather than loop
|
|
1917
|
+
// again. Deliver the latest meaningful output we have.
|
|
1918
|
+
if (wasStopped()) {
|
|
1919
|
+
console.log(`[${this.config.name}] auto-retry: aborted by /stop mid-probe for ${chatId.slice(-8)}`);
|
|
1920
|
+
const latest = probeText && probeText.toUpperCase() !== "NO_REPLY" ? probeReply : reply;
|
|
1921
|
+
if (!latest)
|
|
1922
|
+
throw new Error(`Agent error: ${params.initialError || "stopped by user"}`);
|
|
1923
|
+
return latest;
|
|
1924
|
+
}
|
|
1931
1925
|
if (!probeText || probeText.toUpperCase() === "NO_REPLY") {
|
|
1932
1926
|
if (!reply)
|
|
1933
1927
|
throw new Error(`Agent error: ${params.initialError || "no reply after retry"}`);
|
|
@@ -3282,6 +3276,9 @@ export class FeishuBot {
|
|
|
3282
3276
|
async handleStopCommand(chatId, messageId) {
|
|
3283
3277
|
const sessionKey = this.getSessionKey(chatId);
|
|
3284
3278
|
try {
|
|
3279
|
+
// Bump the stop generation FIRST so any in-flight auto-retry loop sees the
|
|
3280
|
+
// cancellation on its next iteration and stops retrying immediately.
|
|
3281
|
+
this.stopEpoch.set(chatId, (this.stopEpoch.get(chatId) || 0) + 1);
|
|
3285
3282
|
// Abort all active runs for this session (chat.abort with no runId).
|
|
3286
3283
|
await this.openclawClient.abortChat(sessionKey).catch(() => { });
|
|
3287
3284
|
// Force-unlock the busy gate so queued messages can be processed.
|