openclaw-lark-multi-agent 1.2.2 → 1.2.3
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 +48 -0
- package/dist/feishu-bot.js +103 -11
- 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,6 +137,23 @@ 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
|
*/
|
|
140
|
+
/**
|
|
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.
|
|
156
|
+
*/
|
|
137
157
|
private looksTruncated;
|
|
138
158
|
/**
|
|
139
159
|
* Did the model type a tool call as plain text instead of emitting a real
|
|
@@ -146,7 +166,35 @@ export declare class FeishuBot {
|
|
|
146
166
|
* sends an explicit "invoke 格式错了,请重试" probe so the agent re-issues the
|
|
147
167
|
* tool call correctly rather than the generic done-check.
|
|
148
168
|
*/
|
|
169
|
+
/**
|
|
170
|
+
* Did the model serialize a tool call as plain text instead of emitting a real
|
|
171
|
+
* function call? When that happens, the structured tool-call XML leaks into the
|
|
172
|
+
* visible text channel and the tool never runs. We must distinguish this from a
|
|
173
|
+
* reply that merely *mentions* a tag (teaching, quoting, showing an example),
|
|
174
|
+
* which must NOT trigger a retry. So we require strong STRUCTURAL evidence of a
|
|
175
|
+
* leaked call, not just any tag at the end:
|
|
176
|
+
*
|
|
177
|
+
* 1. The reply must END with a closing `</invoke>` (optionally `antml:`).
|
|
178
|
+
* A dangling OPEN tag or a bare `</parameter>`/`<function_calls>` alone is
|
|
179
|
+
* not enough — too easy to hit in normal prose.
|
|
180
|
+
* 2. Somewhere before it there must be a matching `<invoke name="...">` OPEN
|
|
181
|
+
* tag — i.e. a whole call block (`<invoke…>…</invoke>`) materialized as
|
|
182
|
+
* text. A lone `</invoke>` (e.g. “闭合标签是 </invoke>”) is a mention,
|
|
183
|
+
* not a leaked call.
|
|
184
|
+
* 3. The trailing `</invoke>` must NOT be wrapped in inline code/backticks or
|
|
185
|
+
* a fenced code block — those are deliberate examples, not a leak.
|
|
186
|
+
*
|
|
187
|
+
* Anchored at the very end because a genuinely leaked call is the LAST thing in
|
|
188
|
+
* the message (the model stopped right after “emitting” it). This keeps the
|
|
189
|
+
* trigger tight so we do not retry on normal replies that talk about tags.
|
|
190
|
+
*/
|
|
149
191
|
private looksLikeBrokenToolCall;
|
|
192
|
+
/**
|
|
193
|
+
* Is the tag at `tagIndex` inside an inline-code span (`` `…` ``) or a fenced
|
|
194
|
+
* code block (```` ``` ````)? Such tags are deliberate examples, not a leaked
|
|
195
|
+
* tool call, so the broken-tool-call detector must ignore them.
|
|
196
|
+
*/
|
|
197
|
+
private isTrailingTagInCode;
|
|
150
198
|
/** Current context usage percent for a session (totalTokens / contextTokens),
|
|
151
199
|
* or 0 if it cannot be determined. Same basis as /status. */
|
|
152
200
|
private getContextUsagePercent;
|
package/dist/feishu-bot.js
CHANGED
|
@@ -75,6 +75,9 @@ export class FeishuBot {
|
|
|
75
75
|
initializedSessions = new Set();
|
|
76
76
|
/** Per-chat busy lock: timestamp when became busy (0 = not busy) */
|
|
77
77
|
busyChats = new Map();
|
|
78
|
+
/** Per-chat "stop" generation counter. Bumped by /stop so any in-flight
|
|
79
|
+
* auto-retry loop can detect it was cancelled and stop retrying immediately. */
|
|
80
|
+
stopEpoch = new Map();
|
|
78
81
|
/** Per-chat pending reply message IDs (to ack with DONE when their trigger is processed) */
|
|
79
82
|
pendingAckMessages = new Map();
|
|
80
83
|
/** Per-chat pending tool message sends (to await before final reply) */
|
|
@@ -1697,26 +1700,38 @@ export class FeishuBot {
|
|
|
1697
1700
|
* only on cheap text shape here, never on stopReason (the upstream rarely
|
|
1698
1701
|
* provides one for normal-looking-but-truncated finals).
|
|
1699
1702
|
*/
|
|
1703
|
+
/**
|
|
1704
|
+
* Heuristic: does this reply look genuinely truncated / cut off mid-task?
|
|
1705
|
+
*
|
|
1706
|
+
* IMPORTANT (per Stephen): only retry when there is CLEAR evidence the agent
|
|
1707
|
+
* did not finish — do NOT retry just because a reply lacks a trailing period.
|
|
1708
|
+
* Many complete replies legitimately end without sentence punctuation (a terse
|
|
1709
|
+
* “好的”, a bullet list, a path, a number, a closing word). Treating those as
|
|
1710
|
+
* truncated caused needless extra probe rounds, which hurts UX. So we gate on
|
|
1711
|
+
* strong structural signals only:
|
|
1712
|
+
* 1. Unbalanced code fence — almost certainly cut off mid code block.
|
|
1713
|
+
* 2. Ends with a dangling connector / lead-in (“然后”, “接下来”, “let me”…)
|
|
1714
|
+
* or a trailing clause-joining punctuation (comma/colon/semicolon),
|
|
1715
|
+
* which means a sentence was left hanging.
|
|
1716
|
+
* 3. Ends mid-word inside a Latin word right after a connector-less clause
|
|
1717
|
+
* (very rare; only when the last “word” is unusually long), kept minimal.
|
|
1718
|
+
* A plain “no sentence-ending punctuation” is deliberately NOT sufficient.
|
|
1719
|
+
*/
|
|
1700
1720
|
looksTruncated(text) {
|
|
1701
1721
|
const t = (text || "").trim();
|
|
1702
1722
|
if (!t)
|
|
1703
1723
|
return false; // empty/NO_REPLY handled elsewhere
|
|
1704
|
-
// Unbalanced code fence => almost certainly cut off mid-block.
|
|
1724
|
+
// (1) Unbalanced code fence => almost certainly cut off mid-block.
|
|
1705
1725
|
const fences = (t.match(/```/g) || []).length;
|
|
1706
1726
|
if (fences % 2 === 1)
|
|
1707
1727
|
return true;
|
|
1708
|
-
//
|
|
1728
|
+
// (2a) Trailing clause-joining punctuation: a sentence was left hanging.
|
|
1709
1729
|
if (/[,,::、;;]\s*$/u.test(t))
|
|
1710
1730
|
return true;
|
|
1711
|
-
|
|
1731
|
+
// (2b) Ends on an explicit continuation cue / lead-in.
|
|
1732
|
+
if (/(接下来|然后|首先|现在我|让我|下一步|我先|我来|我接着|接着|稍等|let me|now i|next|first|\b(?:and|then|so|to|the|a)\b)\s*$/iu.test(t))
|
|
1712
1733
|
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;
|
|
1734
|
+
return false;
|
|
1720
1735
|
}
|
|
1721
1736
|
/**
|
|
1722
1737
|
* Did the model type a tool call as plain text instead of emitting a real
|
|
@@ -1729,11 +1744,64 @@ export class FeishuBot {
|
|
|
1729
1744
|
* sends an explicit "invoke 格式错了,请重试" probe so the agent re-issues the
|
|
1730
1745
|
* tool call correctly rather than the generic done-check.
|
|
1731
1746
|
*/
|
|
1747
|
+
/**
|
|
1748
|
+
* Did the model serialize a tool call as plain text instead of emitting a real
|
|
1749
|
+
* function call? When that happens, the structured tool-call XML leaks into the
|
|
1750
|
+
* visible text channel and the tool never runs. We must distinguish this from a
|
|
1751
|
+
* reply that merely *mentions* a tag (teaching, quoting, showing an example),
|
|
1752
|
+
* which must NOT trigger a retry. So we require strong STRUCTURAL evidence of a
|
|
1753
|
+
* leaked call, not just any tag at the end:
|
|
1754
|
+
*
|
|
1755
|
+
* 1. The reply must END with a closing `</invoke>` (optionally `antml:`).
|
|
1756
|
+
* A dangling OPEN tag or a bare `</parameter>`/`<function_calls>` alone is
|
|
1757
|
+
* not enough — too easy to hit in normal prose.
|
|
1758
|
+
* 2. Somewhere before it there must be a matching `<invoke name="...">` OPEN
|
|
1759
|
+
* tag — i.e. a whole call block (`<invoke…>…</invoke>`) materialized as
|
|
1760
|
+
* text. A lone `</invoke>` (e.g. “闭合标签是 </invoke>”) is a mention,
|
|
1761
|
+
* not a leaked call.
|
|
1762
|
+
* 3. The trailing `</invoke>` must NOT be wrapped in inline code/backticks or
|
|
1763
|
+
* a fenced code block — those are deliberate examples, not a leak.
|
|
1764
|
+
*
|
|
1765
|
+
* Anchored at the very end because a genuinely leaked call is the LAST thing in
|
|
1766
|
+
* the message (the model stopped right after “emitting” it). This keeps the
|
|
1767
|
+
* trigger tight so we do not retry on normal replies that talk about tags.
|
|
1768
|
+
*/
|
|
1732
1769
|
looksLikeBrokenToolCall(text) {
|
|
1733
1770
|
const t = (text || "").trim();
|
|
1734
1771
|
if (!t)
|
|
1735
1772
|
return false;
|
|
1736
|
-
|
|
1773
|
+
// (1) Must end with a closing invoke tag (the serialized call's terminator).
|
|
1774
|
+
const closeMatch = /<\/\s*(?:antml:)?invoke\s*>\s*$/i.exec(t);
|
|
1775
|
+
if (!closeMatch)
|
|
1776
|
+
return false;
|
|
1777
|
+
// (3) Reject when that closing tag is fenced or inline-code wrapped (example).
|
|
1778
|
+
if (this.isTrailingTagInCode(t, closeMatch.index))
|
|
1779
|
+
return false;
|
|
1780
|
+
// (2) Require a matching opening `<invoke ...>` before the closer — proof that
|
|
1781
|
+
// a whole call block leaked, not just a mention of the closing tag.
|
|
1782
|
+
const head = t.slice(0, closeMatch.index);
|
|
1783
|
+
if (!/<\s*(?:antml:)?invoke\b[^>]*>/i.test(head))
|
|
1784
|
+
return false;
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1787
|
+
/**
|
|
1788
|
+
* Is the tag at `tagIndex` inside an inline-code span (`` `…` ``) or a fenced
|
|
1789
|
+
* code block (```` ``` ````)? Such tags are deliberate examples, not a leaked
|
|
1790
|
+
* tool call, so the broken-tool-call detector must ignore them.
|
|
1791
|
+
*/
|
|
1792
|
+
isTrailingTagInCode(text, tagIndex) {
|
|
1793
|
+
const before = text.slice(0, tagIndex);
|
|
1794
|
+
// Fenced block: an odd number of ``` before the tag means we are inside one.
|
|
1795
|
+
const fences = (before.match(/```/g) || []).length;
|
|
1796
|
+
if (fences % 2 === 1)
|
|
1797
|
+
return true;
|
|
1798
|
+
// Inline code: an odd number of unescaped backticks on the tag's own line.
|
|
1799
|
+
const lineStart = before.lastIndexOf("\n") + 1;
|
|
1800
|
+
const lineBefore = before.slice(lineStart);
|
|
1801
|
+
const ticks = (lineBefore.match(/`/g) || []).length;
|
|
1802
|
+
if (ticks % 2 === 1)
|
|
1803
|
+
return true;
|
|
1804
|
+
return false;
|
|
1737
1805
|
}
|
|
1738
1806
|
/** Current context usage percent for a session (totalTokens / contextTokens),
|
|
1739
1807
|
* or 0 if it cannot be determined. Same basis as /status. */
|
|
@@ -1833,11 +1901,23 @@ export class FeishuBot {
|
|
|
1833
1901
|
const maxRetry = autoRetryMax();
|
|
1834
1902
|
let compacted = false; // auto-compact at most once per auto-retry round
|
|
1835
1903
|
let probedAtLeastOnce = false;
|
|
1904
|
+
// Snapshot the stop generation at entry. If the user runs /stop while this
|
|
1905
|
+
// loop is in flight, handleStopCommand bumps the epoch; we detect the change
|
|
1906
|
+
// and stop retrying immediately instead of issuing more probes.
|
|
1907
|
+
const startStopEpoch = this.stopEpoch.get(chatId) || 0;
|
|
1908
|
+
const wasStopped = () => (this.stopEpoch.get(chatId) || 0) !== startStopEpoch;
|
|
1836
1909
|
// Set per-iteration when the reply under inspection ended with a dangling
|
|
1837
1910
|
// tool-call tag (model typed an invoke as plain text); selects the explicit
|
|
1838
1911
|
// "invoke format was broken, retry" probe instead of the generic done-check.
|
|
1839
1912
|
let brokenToolCall = false;
|
|
1840
1913
|
for (let attempt = 1; attempt <= maxRetry; attempt++) {
|
|
1914
|
+
// /stop while we were looping: abandon retries and deliver what we have.
|
|
1915
|
+
if (wasStopped()) {
|
|
1916
|
+
console.log(`[${this.config.name}] auto-retry: aborted by /stop for ${chatId.slice(-8)}`);
|
|
1917
|
+
if (!reply)
|
|
1918
|
+
throw new Error(`Agent error: ${params.initialError || "stopped by user"}`);
|
|
1919
|
+
return reply;
|
|
1920
|
+
}
|
|
1841
1921
|
brokenToolCall = false;
|
|
1842
1922
|
if (!forceProbe) {
|
|
1843
1923
|
const parsed = this.extractBridgeAttachments(reply);
|
|
@@ -1928,6 +2008,15 @@ export class FeishuBot {
|
|
|
1928
2008
|
return reply;
|
|
1929
2009
|
}
|
|
1930
2010
|
const probeText = this.extractBridgeAttachments(probeReply).text.trim();
|
|
2011
|
+
// /stop landed while the probe was in flight: stop here rather than loop
|
|
2012
|
+
// again. Deliver the latest meaningful output we have.
|
|
2013
|
+
if (wasStopped()) {
|
|
2014
|
+
console.log(`[${this.config.name}] auto-retry: aborted by /stop mid-probe for ${chatId.slice(-8)}`);
|
|
2015
|
+
const latest = probeText && probeText.toUpperCase() !== "NO_REPLY" ? probeReply : reply;
|
|
2016
|
+
if (!latest)
|
|
2017
|
+
throw new Error(`Agent error: ${params.initialError || "stopped by user"}`);
|
|
2018
|
+
return latest;
|
|
2019
|
+
}
|
|
1931
2020
|
if (!probeText || probeText.toUpperCase() === "NO_REPLY") {
|
|
1932
2021
|
if (!reply)
|
|
1933
2022
|
throw new Error(`Agent error: ${params.initialError || "no reply after retry"}`);
|
|
@@ -3282,6 +3371,9 @@ export class FeishuBot {
|
|
|
3282
3371
|
async handleStopCommand(chatId, messageId) {
|
|
3283
3372
|
const sessionKey = this.getSessionKey(chatId);
|
|
3284
3373
|
try {
|
|
3374
|
+
// Bump the stop generation FIRST so any in-flight auto-retry loop sees the
|
|
3375
|
+
// cancellation on its next iteration and stops retrying immediately.
|
|
3376
|
+
this.stopEpoch.set(chatId, (this.stopEpoch.get(chatId) || 0) + 1);
|
|
3285
3377
|
// Abort all active runs for this session (chat.abort with no runId).
|
|
3286
3378
|
await this.openclawClient.abortChat(sessionKey).catch(() => { });
|
|
3287
3379
|
// Force-unlock the busy gate so queued messages can be processed.
|