larkway 0.3.15 → 0.3.17
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 +1 -1
- package/README.zh.md +1 -1
- package/dist/cli/index.js +160 -9
- package/dist/main.js +366 -53
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
|
|
10
10
|
|
|
11
|
-
**Current release: v0.3.
|
|
11
|
+
**Current release: v0.3.17**
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/README.zh.md
CHANGED
package/dist/cli/index.js
CHANGED
|
@@ -112093,7 +112093,7 @@ function channelMsgToLarkEvent(msg) {
|
|
|
112093
112093
|
create_time: m?.["create_time"] ?? String(msg.createTime ?? Date.now())
|
|
112094
112094
|
};
|
|
112095
112095
|
}
|
|
112096
|
-
var import_node_sdk, execFile4, LEARNED_CHATS_LIMIT, SEEN_MESSAGES_LIMIT, MAX_MESSAGE_ATTEMPTS, OPEN_CHAT_DISCOVERY_LOOKBACK_MS, OPEN_CHAT_DISCOVERY_BOOTSTRAP_LOOKBACK_MS, PROCESSING_REACTION_EMOJI, DEFAULT_CONNECT_GRACE_MS, ChannelClient;
|
|
112096
|
+
var import_node_sdk, execFile4, LEARNED_CHATS_LIMIT, SEEN_MESSAGES_LIMIT, MAX_MESSAGE_ATTEMPTS, OPEN_CHAT_DISCOVERY_LOOKBACK_MS, OPEN_CHAT_DISCOVERY_BOOTSTRAP_LOOKBACK_MS, PROCESSING_REACTION_EMOJI, GAP_FILL_MAX_ATTEMPTS, GAP_FILL_BACKOFF_BASE_MS, UNRESOLVED_WINDOW_MAX_CHATS, UNRESOLVED_WINDOW_MAX_AGE_MS, DEFAULT_CONNECT_GRACE_MS, ChannelClient;
|
|
112097
112097
|
var init_channelClient = __esm({
|
|
112098
112098
|
"src/lark/channelClient.ts"() {
|
|
112099
112099
|
"use strict";
|
|
@@ -112107,6 +112107,10 @@ var init_channelClient = __esm({
|
|
|
112107
112107
|
OPEN_CHAT_DISCOVERY_LOOKBACK_MS = 9e4;
|
|
112108
112108
|
OPEN_CHAT_DISCOVERY_BOOTSTRAP_LOOKBACK_MS = 30 * 60 * 1e3;
|
|
112109
112109
|
PROCESSING_REACTION_EMOJI = "Typing";
|
|
112110
|
+
GAP_FILL_MAX_ATTEMPTS = 3;
|
|
112111
|
+
GAP_FILL_BACKOFF_BASE_MS = 1e3;
|
|
112112
|
+
UNRESOLVED_WINDOW_MAX_CHATS = 50;
|
|
112113
|
+
UNRESOLVED_WINDOW_MAX_AGE_MS = 30 * 60 * 1e3;
|
|
112110
112114
|
DEFAULT_CONNECT_GRACE_MS = 3e3;
|
|
112111
112115
|
ChannelClient = class {
|
|
112112
112116
|
opts;
|
|
@@ -112171,6 +112175,27 @@ var init_channelClient = __esm({
|
|
|
112171
112175
|
* search space.
|
|
112172
112176
|
*/
|
|
112173
112177
|
recentlySeenChatIds = /* @__PURE__ */ new Set();
|
|
112178
|
+
/**
|
|
112179
|
+
* PER-CHAT unresolved gapFill windows: chatId → the OLDEST windowStart (ms) for
|
|
112180
|
+
* which that chat's lark-cli history pull still failed after all retries. On a
|
|
112181
|
+
* later gapFill that ACTUALLY pulls this chat, we extend the look-back to cover
|
|
112182
|
+
* its oldest unresolved windowStart and only clear it once the pull truly
|
|
112183
|
+
* reached back that far (its `--start` <= the tracked windowStart). Per-chat (not
|
|
112184
|
+
* a single shared list) so a successful run over chat-set {B} can never falsely
|
|
112185
|
+
* resolve chat A's window (BLOCKER 1), and the look-back-vs-clamp mismatch can
|
|
112186
|
+
* never mark a window resolved before it was reached (BLOCKER 2).
|
|
112187
|
+
*
|
|
112188
|
+
* Bounded: one timestamp per chat (so naturally bounded by #chats), pruned by age
|
|
112189
|
+
* (UNRESOLVED_WINDOW_MAX_AGE_MS — older = unrecoverable from history anyway) and
|
|
112190
|
+
* capped at UNRESOLVED_WINDOW_MAX_CHATS tracked chats.
|
|
112191
|
+
*/
|
|
112192
|
+
unresolvedGapWindowByChat = /* @__PURE__ */ new Map();
|
|
112193
|
+
/**
|
|
112194
|
+
* Backoff sleep used by the per-chat history-pull retry. Indirected through a
|
|
112195
|
+
* field purely so tests can observe/await the backoff deterministically; in
|
|
112196
|
+
* production it is the real timer-based {@link sleep}.
|
|
112197
|
+
*/
|
|
112198
|
+
gapFillSleep = sleep;
|
|
112174
112199
|
openChatDiscoveryTimer = null;
|
|
112175
112200
|
openChatDiscoveryRunning = false;
|
|
112176
112201
|
openChatDiscoveryBootstrapped = false;
|
|
@@ -112192,6 +112217,28 @@ var init_channelClient = __esm({
|
|
|
112192
112217
|
}
|
|
112193
112218
|
this.opts = opts;
|
|
112194
112219
|
}
|
|
112220
|
+
/**
|
|
112221
|
+
* TEST SEAM (deletable): override the gap-fill retry backoff sleep so unit
|
|
112222
|
+
* tests can observe the backoff durations and avoid real timers. No-op for
|
|
112223
|
+
* production — the default is the real {@link sleep}. Returns the recorded
|
|
112224
|
+
* backoff arg via the provided callback's own bookkeeping.
|
|
112225
|
+
*/
|
|
112226
|
+
setGapFillSleepForTest(fn) {
|
|
112227
|
+
this.gapFillSleep = fn;
|
|
112228
|
+
}
|
|
112229
|
+
/** TEST-ONLY read of the per-chat unresolved-window replay map (chatId → windowStart). */
|
|
112230
|
+
unresolvedGapWindowsForTest() {
|
|
112231
|
+
return new Map(this.unresolvedGapWindowByChat);
|
|
112232
|
+
}
|
|
112233
|
+
/**
|
|
112234
|
+
* TEST-ONLY direct gapFill invocation with an explicit chat-set override —
|
|
112235
|
+
* mirrors exactly how open-chat discovery calls gapFill on a SUBSET of chats.
|
|
112236
|
+
* Used to reproduce the cross-chat-set replay isolation (BLOCKER 1) without
|
|
112237
|
+
* standing up the full discovery timer.
|
|
112238
|
+
*/
|
|
112239
|
+
async gapFillForTest(disconnectAt, chatIds) {
|
|
112240
|
+
await this.gapFill(disconnectAt, (s) => console.log(`[channel.client] ${s}`), chatIds);
|
|
112241
|
+
}
|
|
112195
112242
|
/**
|
|
112196
112243
|
* Async iterator over inbound events — interface-compatible with LarkClient.
|
|
112197
112244
|
* Connects the WS on first call. The SDK's policy gate (requireMention +
|
|
@@ -112257,7 +112304,25 @@ var init_channelClient = __esm({
|
|
|
112257
112304
|
// ── WS robustness knobs (node-sdk ≥1.64; all OFF by default) ──────────
|
|
112258
112305
|
// Abort a handshake that hangs on a stuck DNS/proxy/NAT path so the retry
|
|
112259
112306
|
// loop can try again, instead of waiting indefinitely. Successful TLS
|
|
112260
|
-
// handshakes are tens of ms; 15s is a wide safety margin.
|
|
112307
|
+
// handshakes are tens of ms; 15s is a wide safety margin. KEPT ON: it is
|
|
112308
|
+
// the right behaviour (abort + reconnect beats hanging forever).
|
|
112309
|
+
//
|
|
112310
|
+
// CAVEAT — raw '_WebSocket' 'error' on abort: when this timeout fires the
|
|
112311
|
+
// SDK aborts the underlying ws, which emits a RAW 'error' event on the
|
|
112312
|
+
// socket. That socket is owned privately inside node-sdk's WSClient
|
|
112313
|
+
// (no public `on()`, no accessor for the raw ws — verified against
|
|
112314
|
+
// node-sdk 1.67.0 types: WSClient is not an EventEmitter and keeps the
|
|
112315
|
+
// `_WebSocket` in a closure), so we CANNOT attach a precise 'error'
|
|
112316
|
+
// listener here. With no listener, Node re-throws it as an
|
|
112317
|
+
// uncaughtException → it would kill the whole (multi-bot) process.
|
|
112318
|
+
// → That raw error is instead caught by the process-level crash guard
|
|
112319
|
+
// in main.ts (registerCrashGuard: uncaughtException handler that logs
|
|
112320
|
+
// and never exits). The channel-level `channel.on("error", …)` below
|
|
112321
|
+
// is a DIFFERENT, higher-level error and does NOT cover this raw case.
|
|
112322
|
+
// → Residual uncertainty (left for acceptance load-testing): that the
|
|
112323
|
+
// process guard reliably catches this specific raw abort path under
|
|
112324
|
+
// real network flap. If a future node-sdk / @larksuite/channel exposes
|
|
112325
|
+
// the ws or attaches its own listener, prefer that and drop the guard.
|
|
112261
112326
|
handshakeTimeoutMs: 15e3,
|
|
112262
112327
|
// Liveness watchdog (SECONDS): if no inbound frame arrives within this
|
|
112263
112328
|
// window after the last ping, treat the socket as dead and reconnect —
|
|
@@ -112374,9 +112439,6 @@ var init_channelClient = __esm({
|
|
|
112374
112439
|
const MAX_GAP_FILL_WINDOW_MS = 5 * 60 * 1e3;
|
|
112375
112440
|
const BUFFER_MS = 3e4;
|
|
112376
112441
|
const now = Date.now();
|
|
112377
|
-
const windowStart = Math.max(disconnectAt - BUFFER_MS, now - MAX_GAP_FILL_WINDOW_MS);
|
|
112378
|
-
const startIso = new Date(windowStart).toISOString();
|
|
112379
|
-
const endIso = new Date(now + BUFFER_MS).toISOString();
|
|
112380
112442
|
const larkCli = this.opts.larkCliPath ?? "lark-cli";
|
|
112381
112443
|
const profileArgs = this.opts.larkCliProfile ? ["--profile", this.opts.larkCliProfile] : [];
|
|
112382
112444
|
const botOpenId = this.opts.botOpenId;
|
|
@@ -112386,16 +112448,29 @@ var init_channelClient = __esm({
|
|
|
112386
112448
|
...this.opts.allowedChatIds,
|
|
112387
112449
|
...this.recentlySeenChatIds
|
|
112388
112450
|
]);
|
|
112451
|
+
this.pruneUnresolvedGapWindows(now);
|
|
112452
|
+
let oldestRelevantUnresolved = Infinity;
|
|
112453
|
+
for (const chatId of gapFillChatIds) {
|
|
112454
|
+
const ws = this.unresolvedGapWindowByChat.get(chatId);
|
|
112455
|
+
if (ws !== void 0 && ws < oldestRelevantUnresolved) oldestRelevantUnresolved = ws;
|
|
112456
|
+
}
|
|
112457
|
+
const hasReplay = oldestRelevantUnresolved !== Infinity;
|
|
112458
|
+
const lookBackFrom = Math.min(disconnectAt, hasReplay ? oldestRelevantUnresolved : disconnectAt);
|
|
112459
|
+
const clampCeilingMs = hasReplay ? UNRESOLVED_WINDOW_MAX_AGE_MS : MAX_GAP_FILL_WINDOW_MS;
|
|
112460
|
+
const windowStart = Math.max(lookBackFrom - BUFFER_MS, now - clampCeilingMs);
|
|
112461
|
+
const startIso = new Date(windowStart).toISOString();
|
|
112462
|
+
const endIso = new Date(now + BUFFER_MS).toISOString();
|
|
112389
112463
|
if (gapFillChatIds.size === 0) {
|
|
112390
112464
|
log(
|
|
112391
112465
|
`gap-fill skipped: no known chats for window=${startIso}..${endIso} (allowedChatIds is empty and no live chat has been seen yet)`
|
|
112392
112466
|
);
|
|
112393
112467
|
return;
|
|
112394
112468
|
}
|
|
112469
|
+
let anyChatFailed = false;
|
|
112395
112470
|
for (const chatId of gapFillChatIds) {
|
|
112396
112471
|
if (this.closed) break;
|
|
112397
112472
|
try {
|
|
112398
|
-
const
|
|
112473
|
+
const args = [
|
|
112399
112474
|
"im",
|
|
112400
112475
|
"+chat-messages-list",
|
|
112401
112476
|
"--as",
|
|
@@ -112414,7 +112489,8 @@ var init_channelClient = __esm({
|
|
|
112414
112489
|
"json",
|
|
112415
112490
|
"--no-reactions",
|
|
112416
112491
|
...profileArgs
|
|
112417
|
-
]
|
|
112492
|
+
];
|
|
112493
|
+
const { stdout: stdout2 } = await this.execWithRetry(larkCli, args, chatId, log);
|
|
112418
112494
|
let messages;
|
|
112419
112495
|
try {
|
|
112420
112496
|
messages = parseLarkCliMessages(stdout2) ?? [];
|
|
@@ -112482,16 +112558,91 @@ var init_channelClient = __esm({
|
|
|
112482
112558
|
this.queue.push(ev);
|
|
112483
112559
|
totalDispatched++;
|
|
112484
112560
|
}
|
|
112561
|
+
this.resolveUnresolvedGapWindow(chatId, windowStart);
|
|
112485
112562
|
} catch (e) {
|
|
112563
|
+
anyChatFailed = true;
|
|
112564
|
+
this.recordUnresolvedGapWindow(chatId, windowStart, log);
|
|
112486
112565
|
log(
|
|
112487
|
-
`gap-fill: lark-cli failed for chat ${chatId}: ` + (e instanceof Error ? e.message : String(e))
|
|
112566
|
+
`gap-fill: lark-cli failed for chat ${chatId} after ${GAP_FILL_MAX_ATTEMPTS} attempt(s): ` + (e instanceof Error ? e.message : String(e))
|
|
112488
112567
|
);
|
|
112489
112568
|
}
|
|
112490
112569
|
}
|
|
112491
112570
|
log(
|
|
112492
|
-
`gap-fill complete: window=${startIso}..${endIso}, fetched=${totalFetched}, dispatched=${totalDispatched}`
|
|
112571
|
+
`gap-fill complete: window=${startIso}..${endIso}, fetched=${totalFetched}, dispatched=${totalDispatched}` + (anyChatFailed ? ` (some chats failed \u2014 per-chat windows queued for replay)` : ``)
|
|
112493
112572
|
);
|
|
112494
112573
|
}
|
|
112574
|
+
/**
|
|
112575
|
+
* Run a lark-cli history pull with bounded retries + exponential backoff.
|
|
112576
|
+
* Retries on ANY thrown error (transient TLS timeout being the motivating case),
|
|
112577
|
+
* up to {@link GAP_FILL_MAX_ATTEMPTS}. Backoff is GAP_FILL_BACKOFF_BASE_MS *
|
|
112578
|
+
* 2^(attempt-1) (~1s / 2s). Re-throws the last error if all attempts fail so the
|
|
112579
|
+
* caller can flag the window for replay. Backoff goes through {@link gapFillSleep}
|
|
112580
|
+
* (injectable) so tests can observe it deterministically.
|
|
112581
|
+
*/
|
|
112582
|
+
async execWithRetry(larkCli, args, chatId, log) {
|
|
112583
|
+
let lastErr;
|
|
112584
|
+
for (let attempt = 1; attempt <= GAP_FILL_MAX_ATTEMPTS; attempt++) {
|
|
112585
|
+
if (this.closed) throw lastErr ?? new Error("closed");
|
|
112586
|
+
try {
|
|
112587
|
+
return await execFile4(larkCli, args);
|
|
112588
|
+
} catch (e) {
|
|
112589
|
+
lastErr = e;
|
|
112590
|
+
if (attempt < GAP_FILL_MAX_ATTEMPTS) {
|
|
112591
|
+
const backoffMs = GAP_FILL_BACKOFF_BASE_MS * 2 ** (attempt - 1);
|
|
112592
|
+
log(
|
|
112593
|
+
`gap-fill: lark-cli pull failed for chat ${chatId} (attempt ${attempt}/${GAP_FILL_MAX_ATTEMPTS}) \u2014 retrying in ${backoffMs}ms: ` + (e instanceof Error ? e.message : String(e))
|
|
112594
|
+
);
|
|
112595
|
+
await this.gapFillSleep(backoffMs);
|
|
112596
|
+
}
|
|
112597
|
+
}
|
|
112598
|
+
}
|
|
112599
|
+
throw lastErr;
|
|
112600
|
+
}
|
|
112601
|
+
/** Drop per-chat unresolved windows older than the replay max age (unrecoverable). */
|
|
112602
|
+
pruneUnresolvedGapWindows(now) {
|
|
112603
|
+
const cutoff = now - UNRESOLVED_WINDOW_MAX_AGE_MS;
|
|
112604
|
+
for (const [chatId, windowStart] of this.unresolvedGapWindowByChat) {
|
|
112605
|
+
if (windowStart < cutoff) this.unresolvedGapWindowByChat.delete(chatId);
|
|
112606
|
+
}
|
|
112607
|
+
}
|
|
112608
|
+
/**
|
|
112609
|
+
* Record (or keep the OLDEST) unresolved window for a chat whose pull failed.
|
|
112610
|
+
* Bounded by chat count: if the map is at capacity and this is a new chat, we
|
|
112611
|
+
* evict the chat with the NEWEST window (least at risk of aging out) so the
|
|
112612
|
+
* oldest at-risk windows survive to be replayed first.
|
|
112613
|
+
*/
|
|
112614
|
+
recordUnresolvedGapWindow(chatId, windowStart, log) {
|
|
112615
|
+
const existing = this.unresolvedGapWindowByChat.get(chatId);
|
|
112616
|
+
if (existing !== void 0 && existing <= windowStart) return;
|
|
112617
|
+
if (existing === void 0 && this.unresolvedGapWindowByChat.size >= UNRESOLVED_WINDOW_MAX_CHATS) {
|
|
112618
|
+
let newestChat = null;
|
|
112619
|
+
let newestWs = -Infinity;
|
|
112620
|
+
for (const [c, ws] of this.unresolvedGapWindowByChat) {
|
|
112621
|
+
if (ws > newestWs) {
|
|
112622
|
+
newestWs = ws;
|
|
112623
|
+
newestChat = c;
|
|
112624
|
+
}
|
|
112625
|
+
}
|
|
112626
|
+
if (newestChat !== null && newestWs > windowStart) this.unresolvedGapWindowByChat.delete(newestChat);
|
|
112627
|
+
else if (newestChat !== null) return;
|
|
112628
|
+
}
|
|
112629
|
+
this.unresolvedGapWindowByChat.set(chatId, windowStart);
|
|
112630
|
+
log(
|
|
112631
|
+
`gap-fill: queued unresolved window for chat ${chatId} start=${new Date(windowStart).toISOString()} (tracked chats=${this.unresolvedGapWindowByChat.size})`
|
|
112632
|
+
);
|
|
112633
|
+
}
|
|
112634
|
+
/**
|
|
112635
|
+
* Resolve a chat's unresolved window on a SUCCESSFUL pull — but ONLY if this
|
|
112636
|
+
* run's `coveredFrom` (its lark-cli `--start`) actually reached back to at or
|
|
112637
|
+
* before the tracked windowStart. If the clamp kept `coveredFrom` NEWER than the
|
|
112638
|
+
* tracked window, the old window was NOT really covered → keep it queued so a
|
|
112639
|
+
* later, wider replay can reach it (BLOCKER 2).
|
|
112640
|
+
*/
|
|
112641
|
+
resolveUnresolvedGapWindow(chatId, coveredFrom) {
|
|
112642
|
+
const tracked = this.unresolvedGapWindowByChat.get(chatId);
|
|
112643
|
+
if (tracked === void 0) return;
|
|
112644
|
+
if (coveredFrom <= tracked) this.unresolvedGapWindowByChat.delete(chatId);
|
|
112645
|
+
}
|
|
112495
112646
|
startOpenChatDiscovery(log) {
|
|
112496
112647
|
if (this.opts.allowedChatIds.size > 0) return;
|
|
112497
112648
|
if (this.openChatDiscoveryTimer) return;
|
package/dist/main.js
CHANGED
|
@@ -15450,7 +15450,7 @@ var require_object_inspect = __commonJS({
|
|
|
15450
15450
|
} else if (indexOf(seen, obj) >= 0) {
|
|
15451
15451
|
return "[Circular]";
|
|
15452
15452
|
}
|
|
15453
|
-
function
|
|
15453
|
+
function inspect2(value, from, noIndent) {
|
|
15454
15454
|
if (from) {
|
|
15455
15455
|
seen = $arrSlice.call(seen);
|
|
15456
15456
|
seen.push(from);
|
|
@@ -15468,7 +15468,7 @@ var require_object_inspect = __commonJS({
|
|
|
15468
15468
|
}
|
|
15469
15469
|
if (typeof obj === "function" && !isRegExp(obj)) {
|
|
15470
15470
|
var name = nameOf(obj);
|
|
15471
|
-
var keys = arrObjKeys(obj,
|
|
15471
|
+
var keys = arrObjKeys(obj, inspect2);
|
|
15472
15472
|
return "[Function" + (name ? ": " + name : " (anonymous)") + "]" + (keys.length > 0 ? " { " + $join.call(keys, ", ") + " }" : "");
|
|
15473
15473
|
}
|
|
15474
15474
|
if (isSymbol(obj)) {
|
|
@@ -15492,16 +15492,16 @@ var require_object_inspect = __commonJS({
|
|
|
15492
15492
|
if (obj.length === 0) {
|
|
15493
15493
|
return "[]";
|
|
15494
15494
|
}
|
|
15495
|
-
var xs = arrObjKeys(obj,
|
|
15495
|
+
var xs = arrObjKeys(obj, inspect2);
|
|
15496
15496
|
if (indent && !singleLineValues(xs)) {
|
|
15497
15497
|
return "[" + indentedJoin(xs, indent) + "]";
|
|
15498
15498
|
}
|
|
15499
15499
|
return "[ " + $join.call(xs, ", ") + " ]";
|
|
15500
15500
|
}
|
|
15501
15501
|
if (isError(obj)) {
|
|
15502
|
-
var parts = arrObjKeys(obj,
|
|
15502
|
+
var parts = arrObjKeys(obj, inspect2);
|
|
15503
15503
|
if (!("cause" in Error.prototype) && "cause" in obj && !isEnumerable.call(obj, "cause")) {
|
|
15504
|
-
return "{ [" + String(obj) + "] " + $join.call($concat.call("[cause]: " +
|
|
15504
|
+
return "{ [" + String(obj) + "] " + $join.call($concat.call("[cause]: " + inspect2(obj.cause), parts), ", ") + " }";
|
|
15505
15505
|
}
|
|
15506
15506
|
if (parts.length === 0) {
|
|
15507
15507
|
return "[" + String(obj) + "]";
|
|
@@ -15519,7 +15519,7 @@ var require_object_inspect = __commonJS({
|
|
|
15519
15519
|
var mapParts = [];
|
|
15520
15520
|
if (mapForEach) {
|
|
15521
15521
|
mapForEach.call(obj, function(value, key) {
|
|
15522
|
-
mapParts.push(
|
|
15522
|
+
mapParts.push(inspect2(key, obj, true) + " => " + inspect2(value, obj));
|
|
15523
15523
|
});
|
|
15524
15524
|
}
|
|
15525
15525
|
return collectionOf("Map", mapSize.call(obj), mapParts, indent);
|
|
@@ -15528,7 +15528,7 @@ var require_object_inspect = __commonJS({
|
|
|
15528
15528
|
var setParts = [];
|
|
15529
15529
|
if (setForEach) {
|
|
15530
15530
|
setForEach.call(obj, function(value) {
|
|
15531
|
-
setParts.push(
|
|
15531
|
+
setParts.push(inspect2(value, obj));
|
|
15532
15532
|
});
|
|
15533
15533
|
}
|
|
15534
15534
|
return collectionOf("Set", setSize.call(obj), setParts, indent);
|
|
@@ -15543,16 +15543,16 @@ var require_object_inspect = __commonJS({
|
|
|
15543
15543
|
return weakCollectionOf("WeakRef");
|
|
15544
15544
|
}
|
|
15545
15545
|
if (isNumber(obj)) {
|
|
15546
|
-
return markBoxed(
|
|
15546
|
+
return markBoxed(inspect2(Number(obj)));
|
|
15547
15547
|
}
|
|
15548
15548
|
if (isBigInt(obj)) {
|
|
15549
|
-
return markBoxed(
|
|
15549
|
+
return markBoxed(inspect2(bigIntValueOf.call(obj)));
|
|
15550
15550
|
}
|
|
15551
15551
|
if (isBoolean2(obj)) {
|
|
15552
15552
|
return markBoxed(booleanValueOf.call(obj));
|
|
15553
15553
|
}
|
|
15554
15554
|
if (isString(obj)) {
|
|
15555
|
-
return markBoxed(
|
|
15555
|
+
return markBoxed(inspect2(String(obj)));
|
|
15556
15556
|
}
|
|
15557
15557
|
if (typeof window !== "undefined" && obj === window) {
|
|
15558
15558
|
return "{ [object Window] }";
|
|
@@ -15561,7 +15561,7 @@ var require_object_inspect = __commonJS({
|
|
|
15561
15561
|
return "{ [object globalThis] }";
|
|
15562
15562
|
}
|
|
15563
15563
|
if (!isDate(obj) && !isRegExp(obj)) {
|
|
15564
|
-
var ys = arrObjKeys(obj,
|
|
15564
|
+
var ys = arrObjKeys(obj, inspect2);
|
|
15565
15565
|
var isPlainObject = gPO ? gPO(obj) === Object.prototype : obj instanceof Object || obj.constructor === Object;
|
|
15566
15566
|
var protoTag = obj instanceof Object ? "" : "null prototype";
|
|
15567
15567
|
var stringTag = !isPlainObject && toStringTag && Object(obj) === obj && toStringTag in obj ? $slice.call(toStr(obj), 8, -1) : protoTag ? "Object" : "";
|
|
@@ -15815,13 +15815,13 @@ var require_object_inspect = __commonJS({
|
|
|
15815
15815
|
var lineJoiner = "\n" + indent.prev + indent.base;
|
|
15816
15816
|
return lineJoiner + $join.call(xs, "," + lineJoiner) + "\n" + indent.prev;
|
|
15817
15817
|
}
|
|
15818
|
-
function arrObjKeys(obj,
|
|
15818
|
+
function arrObjKeys(obj, inspect2) {
|
|
15819
15819
|
var isArr = isArray(obj);
|
|
15820
15820
|
var xs = [];
|
|
15821
15821
|
if (isArr) {
|
|
15822
15822
|
xs.length = obj.length;
|
|
15823
15823
|
for (var i = 0; i < obj.length; i++) {
|
|
15824
|
-
xs[i] = has(obj, i) ?
|
|
15824
|
+
xs[i] = has(obj, i) ? inspect2(obj[i], obj) : "";
|
|
15825
15825
|
}
|
|
15826
15826
|
}
|
|
15827
15827
|
var syms = typeof gOPS === "function" ? gOPS(obj) : [];
|
|
@@ -15842,15 +15842,15 @@ var require_object_inspect = __commonJS({
|
|
|
15842
15842
|
if (hasShammedSymbols && symMap["$" + key] instanceof Symbol) {
|
|
15843
15843
|
continue;
|
|
15844
15844
|
} else if ($test.call(/[^\w$]/, key)) {
|
|
15845
|
-
xs.push(
|
|
15845
|
+
xs.push(inspect2(key, obj) + ": " + inspect2(obj[key], obj));
|
|
15846
15846
|
} else {
|
|
15847
|
-
xs.push(key + ": " +
|
|
15847
|
+
xs.push(key + ": " + inspect2(obj[key], obj));
|
|
15848
15848
|
}
|
|
15849
15849
|
}
|
|
15850
15850
|
if (typeof gOPS === "function") {
|
|
15851
15851
|
for (var j = 0; j < syms.length; j++) {
|
|
15852
15852
|
if (isEnumerable.call(obj, syms[j])) {
|
|
15853
|
-
xs.push("[" +
|
|
15853
|
+
xs.push("[" + inspect2(syms[j]) + "]: " + inspect2(obj[syms[j]], obj));
|
|
15854
15854
|
}
|
|
15855
15855
|
}
|
|
15856
15856
|
}
|
|
@@ -15863,7 +15863,7 @@ var require_object_inspect = __commonJS({
|
|
|
15863
15863
|
var require_side_channel_list = __commonJS({
|
|
15864
15864
|
"node_modules/.pnpm/side-channel-list@1.0.1/node_modules/side-channel-list/index.js"(exports, module) {
|
|
15865
15865
|
"use strict";
|
|
15866
|
-
var
|
|
15866
|
+
var inspect2 = require_object_inspect();
|
|
15867
15867
|
var $TypeError = require_type();
|
|
15868
15868
|
var listGetNode = function(list, key, isDelete) {
|
|
15869
15869
|
var prev = list;
|
|
@@ -15917,7 +15917,7 @@ var require_side_channel_list = __commonJS({
|
|
|
15917
15917
|
var channel = {
|
|
15918
15918
|
assert: function(key) {
|
|
15919
15919
|
if (!channel.has(key)) {
|
|
15920
|
-
throw new $TypeError("Side channel does not contain " +
|
|
15920
|
+
throw new $TypeError("Side channel does not contain " + inspect2(key));
|
|
15921
15921
|
}
|
|
15922
15922
|
},
|
|
15923
15923
|
"delete": function(key) {
|
|
@@ -15981,7 +15981,7 @@ var require_side_channel_map = __commonJS({
|
|
|
15981
15981
|
"use strict";
|
|
15982
15982
|
var GetIntrinsic = require_get_intrinsic();
|
|
15983
15983
|
var callBound = require_call_bound();
|
|
15984
|
-
var
|
|
15984
|
+
var inspect2 = require_object_inspect();
|
|
15985
15985
|
var $TypeError = require_type();
|
|
15986
15986
|
var $Map = GetIntrinsic("%Map%", true);
|
|
15987
15987
|
var $mapGet = callBound("Map.prototype.get", true);
|
|
@@ -15995,7 +15995,7 @@ var require_side_channel_map = __commonJS({
|
|
|
15995
15995
|
var channel = {
|
|
15996
15996
|
assert: function(key) {
|
|
15997
15997
|
if (!channel.has(key)) {
|
|
15998
|
-
throw new $TypeError("Side channel does not contain " +
|
|
15998
|
+
throw new $TypeError("Side channel does not contain " + inspect2(key));
|
|
15999
15999
|
}
|
|
16000
16000
|
},
|
|
16001
16001
|
"delete": function(key) {
|
|
@@ -16037,7 +16037,7 @@ var require_side_channel_weakmap = __commonJS({
|
|
|
16037
16037
|
"use strict";
|
|
16038
16038
|
var GetIntrinsic = require_get_intrinsic();
|
|
16039
16039
|
var callBound = require_call_bound();
|
|
16040
|
-
var
|
|
16040
|
+
var inspect2 = require_object_inspect();
|
|
16041
16041
|
var getSideChannelMap = require_side_channel_map();
|
|
16042
16042
|
var $TypeError = require_type();
|
|
16043
16043
|
var $WeakMap = GetIntrinsic("%WeakMap%", true);
|
|
@@ -16053,7 +16053,7 @@ var require_side_channel_weakmap = __commonJS({
|
|
|
16053
16053
|
var channel = {
|
|
16054
16054
|
assert: function(key) {
|
|
16055
16055
|
if (!channel.has(key)) {
|
|
16056
|
-
throw new $TypeError("Side channel does not contain " +
|
|
16056
|
+
throw new $TypeError("Side channel does not contain " + inspect2(key));
|
|
16057
16057
|
}
|
|
16058
16058
|
},
|
|
16059
16059
|
"delete": function(key) {
|
|
@@ -16109,7 +16109,7 @@ var require_side_channel = __commonJS({
|
|
|
16109
16109
|
"node_modules/.pnpm/side-channel@1.1.0/node_modules/side-channel/index.js"(exports, module) {
|
|
16110
16110
|
"use strict";
|
|
16111
16111
|
var $TypeError = require_type();
|
|
16112
|
-
var
|
|
16112
|
+
var inspect2 = require_object_inspect();
|
|
16113
16113
|
var getSideChannelList = require_side_channel_list();
|
|
16114
16114
|
var getSideChannelMap = require_side_channel_map();
|
|
16115
16115
|
var getSideChannelWeakMap = require_side_channel_weakmap();
|
|
@@ -16119,7 +16119,7 @@ var require_side_channel = __commonJS({
|
|
|
16119
16119
|
var channel = {
|
|
16120
16120
|
assert: function(key) {
|
|
16121
16121
|
if (!channel.has(key)) {
|
|
16122
|
-
throw new $TypeError("Side channel does not contain " +
|
|
16122
|
+
throw new $TypeError("Side channel does not contain " + inspect2(key));
|
|
16123
16123
|
}
|
|
16124
16124
|
},
|
|
16125
16125
|
"delete": function(key) {
|
|
@@ -116182,6 +116182,10 @@ var MAX_MESSAGE_ATTEMPTS = 5;
|
|
|
116182
116182
|
var OPEN_CHAT_DISCOVERY_LOOKBACK_MS = 9e4;
|
|
116183
116183
|
var OPEN_CHAT_DISCOVERY_BOOTSTRAP_LOOKBACK_MS = 30 * 60 * 1e3;
|
|
116184
116184
|
var PROCESSING_REACTION_EMOJI = "Typing";
|
|
116185
|
+
var GAP_FILL_MAX_ATTEMPTS = 3;
|
|
116186
|
+
var GAP_FILL_BACKOFF_BASE_MS = 1e3;
|
|
116187
|
+
var UNRESOLVED_WINDOW_MAX_CHATS = 50;
|
|
116188
|
+
var UNRESOLVED_WINDOW_MAX_AGE_MS = 30 * 60 * 1e3;
|
|
116185
116189
|
function stripAtMarkup(s) {
|
|
116186
116190
|
return s.replace(/<at\b[^>]*>.*?<\/at>/gi, "").replace(/<at\b[^>]*\/>/gi, "").trim();
|
|
116187
116191
|
}
|
|
@@ -116382,6 +116386,27 @@ var ChannelClient = class {
|
|
|
116382
116386
|
* search space.
|
|
116383
116387
|
*/
|
|
116384
116388
|
recentlySeenChatIds = /* @__PURE__ */ new Set();
|
|
116389
|
+
/**
|
|
116390
|
+
* PER-CHAT unresolved gapFill windows: chatId → the OLDEST windowStart (ms) for
|
|
116391
|
+
* which that chat's lark-cli history pull still failed after all retries. On a
|
|
116392
|
+
* later gapFill that ACTUALLY pulls this chat, we extend the look-back to cover
|
|
116393
|
+
* its oldest unresolved windowStart and only clear it once the pull truly
|
|
116394
|
+
* reached back that far (its `--start` <= the tracked windowStart). Per-chat (not
|
|
116395
|
+
* a single shared list) so a successful run over chat-set {B} can never falsely
|
|
116396
|
+
* resolve chat A's window (BLOCKER 1), and the look-back-vs-clamp mismatch can
|
|
116397
|
+
* never mark a window resolved before it was reached (BLOCKER 2).
|
|
116398
|
+
*
|
|
116399
|
+
* Bounded: one timestamp per chat (so naturally bounded by #chats), pruned by age
|
|
116400
|
+
* (UNRESOLVED_WINDOW_MAX_AGE_MS — older = unrecoverable from history anyway) and
|
|
116401
|
+
* capped at UNRESOLVED_WINDOW_MAX_CHATS tracked chats.
|
|
116402
|
+
*/
|
|
116403
|
+
unresolvedGapWindowByChat = /* @__PURE__ */ new Map();
|
|
116404
|
+
/**
|
|
116405
|
+
* Backoff sleep used by the per-chat history-pull retry. Indirected through a
|
|
116406
|
+
* field purely so tests can observe/await the backoff deterministically; in
|
|
116407
|
+
* production it is the real timer-based {@link sleep}.
|
|
116408
|
+
*/
|
|
116409
|
+
gapFillSleep = sleep;
|
|
116385
116410
|
openChatDiscoveryTimer = null;
|
|
116386
116411
|
openChatDiscoveryRunning = false;
|
|
116387
116412
|
openChatDiscoveryBootstrapped = false;
|
|
@@ -116403,6 +116428,28 @@ var ChannelClient = class {
|
|
|
116403
116428
|
}
|
|
116404
116429
|
this.opts = opts;
|
|
116405
116430
|
}
|
|
116431
|
+
/**
|
|
116432
|
+
* TEST SEAM (deletable): override the gap-fill retry backoff sleep so unit
|
|
116433
|
+
* tests can observe the backoff durations and avoid real timers. No-op for
|
|
116434
|
+
* production — the default is the real {@link sleep}. Returns the recorded
|
|
116435
|
+
* backoff arg via the provided callback's own bookkeeping.
|
|
116436
|
+
*/
|
|
116437
|
+
setGapFillSleepForTest(fn) {
|
|
116438
|
+
this.gapFillSleep = fn;
|
|
116439
|
+
}
|
|
116440
|
+
/** TEST-ONLY read of the per-chat unresolved-window replay map (chatId → windowStart). */
|
|
116441
|
+
unresolvedGapWindowsForTest() {
|
|
116442
|
+
return new Map(this.unresolvedGapWindowByChat);
|
|
116443
|
+
}
|
|
116444
|
+
/**
|
|
116445
|
+
* TEST-ONLY direct gapFill invocation with an explicit chat-set override —
|
|
116446
|
+
* mirrors exactly how open-chat discovery calls gapFill on a SUBSET of chats.
|
|
116447
|
+
* Used to reproduce the cross-chat-set replay isolation (BLOCKER 1) without
|
|
116448
|
+
* standing up the full discovery timer.
|
|
116449
|
+
*/
|
|
116450
|
+
async gapFillForTest(disconnectAt, chatIds) {
|
|
116451
|
+
await this.gapFill(disconnectAt, (s) => console.log(`[channel.client] ${s}`), chatIds);
|
|
116452
|
+
}
|
|
116406
116453
|
/**
|
|
116407
116454
|
* Async iterator over inbound events — interface-compatible with LarkClient.
|
|
116408
116455
|
* Connects the WS on first call. The SDK's policy gate (requireMention +
|
|
@@ -116468,7 +116515,25 @@ var ChannelClient = class {
|
|
|
116468
116515
|
// ── WS robustness knobs (node-sdk ≥1.64; all OFF by default) ──────────
|
|
116469
116516
|
// Abort a handshake that hangs on a stuck DNS/proxy/NAT path so the retry
|
|
116470
116517
|
// loop can try again, instead of waiting indefinitely. Successful TLS
|
|
116471
|
-
// handshakes are tens of ms; 15s is a wide safety margin.
|
|
116518
|
+
// handshakes are tens of ms; 15s is a wide safety margin. KEPT ON: it is
|
|
116519
|
+
// the right behaviour (abort + reconnect beats hanging forever).
|
|
116520
|
+
//
|
|
116521
|
+
// CAVEAT — raw '_WebSocket' 'error' on abort: when this timeout fires the
|
|
116522
|
+
// SDK aborts the underlying ws, which emits a RAW 'error' event on the
|
|
116523
|
+
// socket. That socket is owned privately inside node-sdk's WSClient
|
|
116524
|
+
// (no public `on()`, no accessor for the raw ws — verified against
|
|
116525
|
+
// node-sdk 1.67.0 types: WSClient is not an EventEmitter and keeps the
|
|
116526
|
+
// `_WebSocket` in a closure), so we CANNOT attach a precise 'error'
|
|
116527
|
+
// listener here. With no listener, Node re-throws it as an
|
|
116528
|
+
// uncaughtException → it would kill the whole (multi-bot) process.
|
|
116529
|
+
// → That raw error is instead caught by the process-level crash guard
|
|
116530
|
+
// in main.ts (registerCrashGuard: uncaughtException handler that logs
|
|
116531
|
+
// and never exits). The channel-level `channel.on("error", …)` below
|
|
116532
|
+
// is a DIFFERENT, higher-level error and does NOT cover this raw case.
|
|
116533
|
+
// → Residual uncertainty (left for acceptance load-testing): that the
|
|
116534
|
+
// process guard reliably catches this specific raw abort path under
|
|
116535
|
+
// real network flap. If a future node-sdk / @larksuite/channel exposes
|
|
116536
|
+
// the ws or attaches its own listener, prefer that and drop the guard.
|
|
116472
116537
|
handshakeTimeoutMs: 15e3,
|
|
116473
116538
|
// Liveness watchdog (SECONDS): if no inbound frame arrives within this
|
|
116474
116539
|
// window after the last ping, treat the socket as dead and reconnect —
|
|
@@ -116585,9 +116650,6 @@ var ChannelClient = class {
|
|
|
116585
116650
|
const MAX_GAP_FILL_WINDOW_MS = 5 * 60 * 1e3;
|
|
116586
116651
|
const BUFFER_MS = 3e4;
|
|
116587
116652
|
const now = Date.now();
|
|
116588
|
-
const windowStart = Math.max(disconnectAt - BUFFER_MS, now - MAX_GAP_FILL_WINDOW_MS);
|
|
116589
|
-
const startIso = new Date(windowStart).toISOString();
|
|
116590
|
-
const endIso = new Date(now + BUFFER_MS).toISOString();
|
|
116591
116653
|
const larkCli = this.opts.larkCliPath ?? "lark-cli";
|
|
116592
116654
|
const profileArgs = this.opts.larkCliProfile ? ["--profile", this.opts.larkCliProfile] : [];
|
|
116593
116655
|
const botOpenId = this.opts.botOpenId;
|
|
@@ -116597,16 +116659,29 @@ var ChannelClient = class {
|
|
|
116597
116659
|
...this.opts.allowedChatIds,
|
|
116598
116660
|
...this.recentlySeenChatIds
|
|
116599
116661
|
]);
|
|
116662
|
+
this.pruneUnresolvedGapWindows(now);
|
|
116663
|
+
let oldestRelevantUnresolved = Infinity;
|
|
116664
|
+
for (const chatId of gapFillChatIds) {
|
|
116665
|
+
const ws = this.unresolvedGapWindowByChat.get(chatId);
|
|
116666
|
+
if (ws !== void 0 && ws < oldestRelevantUnresolved) oldestRelevantUnresolved = ws;
|
|
116667
|
+
}
|
|
116668
|
+
const hasReplay = oldestRelevantUnresolved !== Infinity;
|
|
116669
|
+
const lookBackFrom = Math.min(disconnectAt, hasReplay ? oldestRelevantUnresolved : disconnectAt);
|
|
116670
|
+
const clampCeilingMs = hasReplay ? UNRESOLVED_WINDOW_MAX_AGE_MS : MAX_GAP_FILL_WINDOW_MS;
|
|
116671
|
+
const windowStart = Math.max(lookBackFrom - BUFFER_MS, now - clampCeilingMs);
|
|
116672
|
+
const startIso = new Date(windowStart).toISOString();
|
|
116673
|
+
const endIso = new Date(now + BUFFER_MS).toISOString();
|
|
116600
116674
|
if (gapFillChatIds.size === 0) {
|
|
116601
116675
|
log(
|
|
116602
116676
|
`gap-fill skipped: no known chats for window=${startIso}..${endIso} (allowedChatIds is empty and no live chat has been seen yet)`
|
|
116603
116677
|
);
|
|
116604
116678
|
return;
|
|
116605
116679
|
}
|
|
116680
|
+
let anyChatFailed = false;
|
|
116606
116681
|
for (const chatId of gapFillChatIds) {
|
|
116607
116682
|
if (this.closed) break;
|
|
116608
116683
|
try {
|
|
116609
|
-
const
|
|
116684
|
+
const args = [
|
|
116610
116685
|
"im",
|
|
116611
116686
|
"+chat-messages-list",
|
|
116612
116687
|
"--as",
|
|
@@ -116625,7 +116700,8 @@ var ChannelClient = class {
|
|
|
116625
116700
|
"json",
|
|
116626
116701
|
"--no-reactions",
|
|
116627
116702
|
...profileArgs
|
|
116628
|
-
]
|
|
116703
|
+
];
|
|
116704
|
+
const { stdout } = await this.execWithRetry(larkCli, args, chatId, log);
|
|
116629
116705
|
let messages;
|
|
116630
116706
|
try {
|
|
116631
116707
|
messages = parseLarkCliMessages(stdout) ?? [];
|
|
@@ -116693,16 +116769,91 @@ var ChannelClient = class {
|
|
|
116693
116769
|
this.queue.push(ev);
|
|
116694
116770
|
totalDispatched++;
|
|
116695
116771
|
}
|
|
116772
|
+
this.resolveUnresolvedGapWindow(chatId, windowStart);
|
|
116696
116773
|
} catch (e) {
|
|
116774
|
+
anyChatFailed = true;
|
|
116775
|
+
this.recordUnresolvedGapWindow(chatId, windowStart, log);
|
|
116697
116776
|
log(
|
|
116698
|
-
`gap-fill: lark-cli failed for chat ${chatId}: ` + (e instanceof Error ? e.message : String(e))
|
|
116777
|
+
`gap-fill: lark-cli failed for chat ${chatId} after ${GAP_FILL_MAX_ATTEMPTS} attempt(s): ` + (e instanceof Error ? e.message : String(e))
|
|
116699
116778
|
);
|
|
116700
116779
|
}
|
|
116701
116780
|
}
|
|
116702
116781
|
log(
|
|
116703
|
-
`gap-fill complete: window=${startIso}..${endIso}, fetched=${totalFetched}, dispatched=${totalDispatched}`
|
|
116782
|
+
`gap-fill complete: window=${startIso}..${endIso}, fetched=${totalFetched}, dispatched=${totalDispatched}` + (anyChatFailed ? ` (some chats failed \u2014 per-chat windows queued for replay)` : ``)
|
|
116783
|
+
);
|
|
116784
|
+
}
|
|
116785
|
+
/**
|
|
116786
|
+
* Run a lark-cli history pull with bounded retries + exponential backoff.
|
|
116787
|
+
* Retries on ANY thrown error (transient TLS timeout being the motivating case),
|
|
116788
|
+
* up to {@link GAP_FILL_MAX_ATTEMPTS}. Backoff is GAP_FILL_BACKOFF_BASE_MS *
|
|
116789
|
+
* 2^(attempt-1) (~1s / 2s). Re-throws the last error if all attempts fail so the
|
|
116790
|
+
* caller can flag the window for replay. Backoff goes through {@link gapFillSleep}
|
|
116791
|
+
* (injectable) so tests can observe it deterministically.
|
|
116792
|
+
*/
|
|
116793
|
+
async execWithRetry(larkCli, args, chatId, log) {
|
|
116794
|
+
let lastErr;
|
|
116795
|
+
for (let attempt = 1; attempt <= GAP_FILL_MAX_ATTEMPTS; attempt++) {
|
|
116796
|
+
if (this.closed) throw lastErr ?? new Error("closed");
|
|
116797
|
+
try {
|
|
116798
|
+
return await execFile(larkCli, args);
|
|
116799
|
+
} catch (e) {
|
|
116800
|
+
lastErr = e;
|
|
116801
|
+
if (attempt < GAP_FILL_MAX_ATTEMPTS) {
|
|
116802
|
+
const backoffMs = GAP_FILL_BACKOFF_BASE_MS * 2 ** (attempt - 1);
|
|
116803
|
+
log(
|
|
116804
|
+
`gap-fill: lark-cli pull failed for chat ${chatId} (attempt ${attempt}/${GAP_FILL_MAX_ATTEMPTS}) \u2014 retrying in ${backoffMs}ms: ` + (e instanceof Error ? e.message : String(e))
|
|
116805
|
+
);
|
|
116806
|
+
await this.gapFillSleep(backoffMs);
|
|
116807
|
+
}
|
|
116808
|
+
}
|
|
116809
|
+
}
|
|
116810
|
+
throw lastErr;
|
|
116811
|
+
}
|
|
116812
|
+
/** Drop per-chat unresolved windows older than the replay max age (unrecoverable). */
|
|
116813
|
+
pruneUnresolvedGapWindows(now) {
|
|
116814
|
+
const cutoff = now - UNRESOLVED_WINDOW_MAX_AGE_MS;
|
|
116815
|
+
for (const [chatId, windowStart] of this.unresolvedGapWindowByChat) {
|
|
116816
|
+
if (windowStart < cutoff) this.unresolvedGapWindowByChat.delete(chatId);
|
|
116817
|
+
}
|
|
116818
|
+
}
|
|
116819
|
+
/**
|
|
116820
|
+
* Record (or keep the OLDEST) unresolved window for a chat whose pull failed.
|
|
116821
|
+
* Bounded by chat count: if the map is at capacity and this is a new chat, we
|
|
116822
|
+
* evict the chat with the NEWEST window (least at risk of aging out) so the
|
|
116823
|
+
* oldest at-risk windows survive to be replayed first.
|
|
116824
|
+
*/
|
|
116825
|
+
recordUnresolvedGapWindow(chatId, windowStart, log) {
|
|
116826
|
+
const existing = this.unresolvedGapWindowByChat.get(chatId);
|
|
116827
|
+
if (existing !== void 0 && existing <= windowStart) return;
|
|
116828
|
+
if (existing === void 0 && this.unresolvedGapWindowByChat.size >= UNRESOLVED_WINDOW_MAX_CHATS) {
|
|
116829
|
+
let newestChat = null;
|
|
116830
|
+
let newestWs = -Infinity;
|
|
116831
|
+
for (const [c, ws] of this.unresolvedGapWindowByChat) {
|
|
116832
|
+
if (ws > newestWs) {
|
|
116833
|
+
newestWs = ws;
|
|
116834
|
+
newestChat = c;
|
|
116835
|
+
}
|
|
116836
|
+
}
|
|
116837
|
+
if (newestChat !== null && newestWs > windowStart) this.unresolvedGapWindowByChat.delete(newestChat);
|
|
116838
|
+
else if (newestChat !== null) return;
|
|
116839
|
+
}
|
|
116840
|
+
this.unresolvedGapWindowByChat.set(chatId, windowStart);
|
|
116841
|
+
log(
|
|
116842
|
+
`gap-fill: queued unresolved window for chat ${chatId} start=${new Date(windowStart).toISOString()} (tracked chats=${this.unresolvedGapWindowByChat.size})`
|
|
116704
116843
|
);
|
|
116705
116844
|
}
|
|
116845
|
+
/**
|
|
116846
|
+
* Resolve a chat's unresolved window on a SUCCESSFUL pull — but ONLY if this
|
|
116847
|
+
* run's `coveredFrom` (its lark-cli `--start`) actually reached back to at or
|
|
116848
|
+
* before the tracked windowStart. If the clamp kept `coveredFrom` NEWER than the
|
|
116849
|
+
* tracked window, the old window was NOT really covered → keep it queued so a
|
|
116850
|
+
* later, wider replay can reach it (BLOCKER 2).
|
|
116851
|
+
*/
|
|
116852
|
+
resolveUnresolvedGapWindow(chatId, coveredFrom) {
|
|
116853
|
+
const tracked = this.unresolvedGapWindowByChat.get(chatId);
|
|
116854
|
+
if (tracked === void 0) return;
|
|
116855
|
+
if (coveredFrom <= tracked) this.unresolvedGapWindowByChat.delete(chatId);
|
|
116856
|
+
}
|
|
116706
116857
|
startOpenChatDiscovery(log) {
|
|
116707
116858
|
if (this.opts.allowedChatIds.size > 0) return;
|
|
116708
116859
|
if (this.openChatDiscoveryTimer) return;
|
|
@@ -116989,6 +117140,7 @@ function summarizeInput(input) {
|
|
|
116989
117140
|
}
|
|
116990
117141
|
return s.length > 60 ? s.slice(0, 57) + "\u2026" : s;
|
|
116991
117142
|
}
|
|
117143
|
+
var MAX_CARD_ELEMENTS = 50;
|
|
116992
117144
|
var CHOICE_MARKERS = ["A", "B", "C", "D", "E"];
|
|
116993
117145
|
function choiceMarker(i) {
|
|
116994
117146
|
return CHOICE_MARKERS[i] ?? String(i + 1);
|
|
@@ -117014,6 +117166,65 @@ function buildChoiceRow(choices) {
|
|
|
117014
117166
|
function buildChoiceLegend(choices) {
|
|
117015
117167
|
return choices.map((c, i) => `**${choiceMarker(i)}.** ${c.label}`).join("\n");
|
|
117016
117168
|
}
|
|
117169
|
+
function plainText(content) {
|
|
117170
|
+
return { tag: "plain_text", content };
|
|
117171
|
+
}
|
|
117172
|
+
function buildImageElement(block) {
|
|
117173
|
+
const element = {
|
|
117174
|
+
tag: "img",
|
|
117175
|
+
img_key: block.img_key,
|
|
117176
|
+
alt: plainText(block.alt || "\u56FE\u7247\u9884\u89C8"),
|
|
117177
|
+
scale_type: block.mode,
|
|
117178
|
+
preview: block.preview
|
|
117179
|
+
};
|
|
117180
|
+
if (block.title) {
|
|
117181
|
+
element["title"] = plainText(block.title);
|
|
117182
|
+
}
|
|
117183
|
+
return element;
|
|
117184
|
+
}
|
|
117185
|
+
function canPushElement(elements, reservedTail = 0) {
|
|
117186
|
+
return elements.length + reservedTail < MAX_CARD_ELEMENTS;
|
|
117187
|
+
}
|
|
117188
|
+
function pushElement(elements, element, reservedTail = 0) {
|
|
117189
|
+
if (!canPushElement(elements, reservedTail)) return false;
|
|
117190
|
+
elements.push(element);
|
|
117191
|
+
return true;
|
|
117192
|
+
}
|
|
117193
|
+
function pushMarkdownElements(elements, text, reservedTail = 0) {
|
|
117194
|
+
const chunks = chunkMarkdown(text);
|
|
117195
|
+
let truncated = false;
|
|
117196
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
117197
|
+
const content = i === 0 ? chunks[i] : `(\u7EED ${i + 1})
|
|
117198
|
+
${chunks[i]}`;
|
|
117199
|
+
if (!pushElement(elements, { tag: "markdown", content }, reservedTail)) {
|
|
117200
|
+
truncated = true;
|
|
117201
|
+
break;
|
|
117202
|
+
}
|
|
117203
|
+
}
|
|
117204
|
+
if (truncated && canPushElement(elements, reservedTail)) {
|
|
117205
|
+
pushElement(elements, {
|
|
117206
|
+
tag: "markdown",
|
|
117207
|
+
content: "_\u5185\u5BB9\u8FC7\u957F,\u540E\u7EED\u90E8\u5206\u5DF2\u7701\u7565_"
|
|
117208
|
+
}, reservedTail);
|
|
117209
|
+
}
|
|
117210
|
+
}
|
|
117211
|
+
function buildContentBlockElements(blocks, opts = {}) {
|
|
117212
|
+
const elements = [];
|
|
117213
|
+
const mentionPrefix = atMentionMarkdown(opts.mentionOpenId);
|
|
117214
|
+
const reservedTail = opts.reservedTail ?? 0;
|
|
117215
|
+
if (mentionPrefix) {
|
|
117216
|
+
pushMarkdownElements(elements, mentionPrefix, reservedTail);
|
|
117217
|
+
}
|
|
117218
|
+
for (const block of blocks) {
|
|
117219
|
+
if (block.type === "markdown") {
|
|
117220
|
+
const clean = stripLeakedToolMarkup(block.content);
|
|
117221
|
+
if (clean) pushMarkdownElements(elements, clean, reservedTail);
|
|
117222
|
+
continue;
|
|
117223
|
+
}
|
|
117224
|
+
pushElement(elements, buildImageElement(block), reservedTail);
|
|
117225
|
+
}
|
|
117226
|
+
return elements;
|
|
117227
|
+
}
|
|
117017
117228
|
function stripLeakedToolMarkup(text) {
|
|
117018
117229
|
return text.replace(/<function_calls>[\s\S]*?<\/function_calls>/gi, "").replace(/<invoke\b[^>]*>[\s\S]*?<\/invoke>/gi, "").replace(/<(?:function_calls|invoke|parameter)\b[\s\S]*$/i, "").replace(/<\/?(?:function_calls|invoke|parameter)\b[^>]*>/gi, "").replace(/\n{3,}/g, "\n\n").trim();
|
|
117019
117230
|
}
|
|
@@ -117046,6 +117257,11 @@ function chunkMarkdown(text, maxLen = 2800) {
|
|
|
117046
117257
|
}
|
|
117047
117258
|
function buildCardJson(opts) {
|
|
117048
117259
|
const elements = [];
|
|
117260
|
+
const hasContentBlocks = !!opts.contentBlocks?.length;
|
|
117261
|
+
const failureReserve = opts.status === "failure" && opts.failureReason ? 2 : 0;
|
|
117262
|
+
const choicesReserve = opts.choices && opts.choices.length ? 4 : 0;
|
|
117263
|
+
const legacyImagesReserve = !hasContentBlocks && opts.imageBlocks?.length ? opts.imageBlocks.length + 1 : 0;
|
|
117264
|
+
const tailReserve = failureReserve + choicesReserve + legacyImagesReserve;
|
|
117049
117265
|
const TOOL_LINES_CAP = 1;
|
|
117050
117266
|
if (opts.showToolSummary && opts.toolLines.length > 0 && !opts.hideTools && opts.status !== "success") {
|
|
117051
117267
|
const total = opts.toolLines.length;
|
|
@@ -117053,44 +117269,52 @@ function buildCardJson(opts) {
|
|
|
117053
117269
|
const omitted = total - recent.length;
|
|
117054
117270
|
const content = omitted > 0 ? `_(\u7565\u524D ${omitted} \u6761\u5DE5\u5177\u8C03\u7528)_
|
|
117055
117271
|
${recent.join("\n")}` : recent.join("\n");
|
|
117056
|
-
elements
|
|
117272
|
+
pushElement(elements, {
|
|
117057
117273
|
tag: "markdown",
|
|
117058
117274
|
content
|
|
117059
117275
|
});
|
|
117060
|
-
elements
|
|
117276
|
+
pushElement(elements, { tag: "hr" });
|
|
117061
117277
|
}
|
|
117062
117278
|
const mentionPrefix = atMentionMarkdown(opts.mentionOpenId);
|
|
117063
117279
|
const cleanBody = opts.bodyText ? stripLeakedToolMarkup(opts.bodyText) : "";
|
|
117064
117280
|
const bodyWithMention = [mentionPrefix, cleanBody].filter(Boolean).join("\n\n");
|
|
117065
|
-
if (
|
|
117066
|
-
|
|
117067
|
-
|
|
117068
|
-
|
|
117069
|
-
|
|
117070
|
-
|
|
117071
|
-
|
|
117072
|
-
|
|
117073
|
-
|
|
117281
|
+
if (hasContentBlocks) {
|
|
117282
|
+
elements.push(
|
|
117283
|
+
...buildContentBlockElements(opts.contentBlocks ?? [], {
|
|
117284
|
+
mentionOpenId: opts.mentionOpenId,
|
|
117285
|
+
reservedTail: tailReserve
|
|
117286
|
+
})
|
|
117287
|
+
);
|
|
117288
|
+
} else if (bodyWithMention) {
|
|
117289
|
+
pushMarkdownElements(elements, bodyWithMention, tailReserve);
|
|
117074
117290
|
} else if (opts.status === "thinking") {
|
|
117075
|
-
elements
|
|
117291
|
+
pushElement(elements, {
|
|
117076
117292
|
tag: "markdown",
|
|
117077
117293
|
content: "\u{1F914} \u601D\u8003\u4E2D\u2026"
|
|
117078
117294
|
});
|
|
117079
117295
|
}
|
|
117080
117296
|
if (opts.status === "failure" && opts.failureReason) {
|
|
117081
|
-
elements
|
|
117082
|
-
elements
|
|
117297
|
+
pushElement(elements, { tag: "hr" }, choicesReserve);
|
|
117298
|
+
pushElement(elements, {
|
|
117083
117299
|
tag: "markdown",
|
|
117084
117300
|
content: `\u26A0\uFE0F **\u9519\u8BEF**: ${opts.failureReason}`
|
|
117085
|
-
});
|
|
117301
|
+
}, choicesReserve);
|
|
117302
|
+
}
|
|
117303
|
+
if (!hasContentBlocks && opts.imageBlocks && opts.imageBlocks.length) {
|
|
117304
|
+
if (bodyWithMention || opts.status === "failure" && opts.failureReason) {
|
|
117305
|
+
pushElement(elements, { tag: "hr" }, choicesReserve);
|
|
117306
|
+
}
|
|
117307
|
+
for (const block of opts.imageBlocks) {
|
|
117308
|
+
pushElement(elements, buildImageElement(block), choicesReserve);
|
|
117309
|
+
}
|
|
117086
117310
|
}
|
|
117087
117311
|
if (opts.choices && opts.choices.length) {
|
|
117088
|
-
elements
|
|
117312
|
+
pushElement(elements, { tag: "hr" });
|
|
117089
117313
|
if (opts.choicePrompt) {
|
|
117090
|
-
elements
|
|
117314
|
+
pushElement(elements, { tag: "markdown", content: opts.choicePrompt });
|
|
117091
117315
|
}
|
|
117092
|
-
elements
|
|
117093
|
-
elements
|
|
117316
|
+
pushElement(elements, { tag: "markdown", content: buildChoiceLegend(opts.choices) });
|
|
117317
|
+
pushElement(elements, buildChoiceRow(opts.choices));
|
|
117094
117318
|
}
|
|
117095
117319
|
const headerColor = opts.status === "thinking" || opts.status === "streaming" ? "blue" : opts.status === "success" ? "green" : "red";
|
|
117096
117320
|
const statusText = opts.status === "thinking" ? "\u23F3 \u5904\u7406\u4E2D" : opts.status === "streaming" ? "\u{1F527} \u5904\u7406\u4E2D" : opts.status === "success" ? "\u2705 \u5B8C\u6210" : "\u274C \u51FA\u9519\u4E86";
|
|
@@ -117165,6 +117389,8 @@ var CardHandleImpl = class {
|
|
|
117165
117389
|
// V2 dynamic-choice buttons — agent-declared, only on finalize.
|
|
117166
117390
|
choices: opts.choices,
|
|
117167
117391
|
choicePrompt: opts.choicePrompt,
|
|
117392
|
+
imageBlocks: opts.imageBlocks,
|
|
117393
|
+
contentBlocks: opts.contentBlocks,
|
|
117168
117394
|
titleOverride: opts.titleOverride,
|
|
117169
117395
|
// Hide tool-use summary on every finalize — the agent finished, so the
|
|
117170
117396
|
// Feishu card should present the result/error only. Diagnostics remain in
|
|
@@ -117877,6 +118103,9 @@ function renderStateContract(stateFilePath) {
|
|
|
117877
118103
|
"- error: \u5931\u8D25\u539F\u56E0(\u642D\u914D status=failed)",
|
|
117878
118104
|
"- card_title: \u5361\u7247\u6807\u9898\u8986\u76D6(\u53EF\u9009)",
|
|
117879
118105
|
"- card_color: \u5361\u7247\u914D\u8272(\u53EF\u9009,success/failure/neutral,\u4E5F\u53EF\u76F4\u63A5\u5199 green/red/grey)",
|
|
118106
|
+
"- image_blocks: \u53EF\u9009\u56FE\u7247\u9884\u89C8\u5757\u6570\u7EC4,\u6700\u591A 4 \u4E2A\u3002\u6BCF\u9879 `{img_key, alt?, title?, mode?, preview?}`; `img_key` \u5FC5\u987B\u662F\u5DF2\u4E0A\u4F20/\u53EF\u7528\u4E8E\u5361\u7247\u7684 Feishu \u56FE\u7247 key,`alt` \u7701\u7565\u65F6 bridge \u9ED8\u8BA4\u201C\u56FE\u7247\u9884\u89C8\u201D,`mode` \u53EA\u5141\u8BB8 `crop_center`/`fit_horizontal` \u5E76\u6620\u5C04\u5230 Card JSON 2.0 `scale_type`,`preview` \u9ED8\u8BA4 true\u3002bridge \u4E0D\u8D1F\u8D23\u4E0B\u8F7D/\u4E0A\u4F20/\u9009\u62E9\u56FE\u7247;\u8FD9\u4E9B\u7531\u4F60\u7528 lark-cli \u7B49\u5DE5\u5177\u5148\u5B8C\u6210\u3002",
|
|
118107
|
+
'- content_blocks: \u53EF\u9009\u6709\u5E8F\u6B63\u6587\u5757\u6570\u7EC4,\u6700\u591A 12 \u4E2A block\u3001\u6700\u591A 4 \u4E2A image block\u3002\u53EA\u652F\u6301\u7A84 union:`{type:"markdown", content}` \u548C `{type:"image", img_key, alt?, title?, mode?, preview?}`;\u4E0D\u652F\u6301 raw card JSON\u3002\u7528\u4E8E\u6B63\u6587\u4E0E\u56FE\u7247\u4EA4\u9519\u6392\u7248,\u4F8B\u5982 markdown -> image -> markdown -> image\u3002\u82E5 `content_blocks` \u975E\u7A7A,bridge \u4EE5\u5B83\u4F5C\u4E3A\u4E3B\u6B63\u6587\u5E76\u5FFD\u7565 `last_message` + `image_blocks` \u7684\u6B63\u6587\u6E32\u67D3,\u907F\u514D\u91CD\u590D;\u82E5\u7701\u7565\u5219\u4FDD\u6301\u65E7 `last_message` + `image_blocks` \u884C\u4E3A\u3002',
|
|
118108
|
+
"- scheduled reply / daily social ops review card \u7B49\u9700\u8981\u201C\u5E73\u53F0\u6B63\u6587 + \u5339\u914D\u56FE\u7247\u201D\u540C\u6BB5\u76F8\u90BB\u5C55\u793A\u7684\u573A\u666F,\u5E94\u5148\u53D6\u5F97\u5404\u56FE\u7247 `img_key`,\u518D\u5199 `content_blocks` \u4E3A `\u5E73\u53F0 markdown -> \u5BF9\u5E94 image -> \u4E0B\u4E2A\u5E73\u53F0 markdown -> \u5BF9\u5E94 image`;\u4E0D\u8981\u7528\u5355\u72EC\u8BDD\u9898\u56FE\u7247\u6D88\u606F\u6216\u5C3E\u90E8 `image_blocks` \u4EE3\u66FF\u9A8C\u6536\u9762\u3002",
|
|
117880
118109
|
"- dev_url / mr_url / \u5176\u4F59\u4E1A\u52A1\u5B57\u6BB5:\u81EA\u7531\u5199\u5165,bridge \u4E0D\u611F\u77E5\u5176\u4E1A\u52A1\u542B\u4E49;\u8981\u8BA9\u8FD0\u8425\u770B\u5230,\u8BF7\u5199\u8FDB last_message",
|
|
117881
118110
|
"- updated_at: ISO 8601 timestamp",
|
|
117882
118111
|
"",
|
|
@@ -117886,7 +118115,7 @@ function renderStateContract(stateFilePath) {
|
|
|
117886
118115
|
"\u6302\u7740\u4E0D\u9000\u51FA = \u5361\u7247\u6C38\u8FDC\u5361\u5728\u300C\u5904\u7406\u4E2D\u300D)\u3002",
|
|
117887
118116
|
"",
|
|
117888
118117
|
"\u5361\u7247\u5C55\u793A\u539F\u5219:",
|
|
117889
|
-
"- bridge \u53EF\u80FD\u5728\u8FD0\u884C\u4E2D\u5C55\u793A\u5DE5\u5177\u6458\u8981,\u8FD9\u662F\u901A\u7528\u8FDB\u5EA6\u4FE1\u53F7;\u6700\u7EC8\u6210\u529F\u5361\u7247\u4EE5\u4F60\u7684 last_message \u4E3A\u4E3B\u3002",
|
|
118118
|
+
"- bridge \u53EF\u80FD\u5728\u8FD0\u884C\u4E2D\u5C55\u793A\u5DE5\u5177\u6458\u8981,\u8FD9\u662F\u901A\u7528\u8FDB\u5EA6\u4FE1\u53F7;\u6700\u7EC8\u6210\u529F\u5361\u7247\u4EE5\u4F60\u7684 `content_blocks`(\u82E5\u975E\u7A7A)\u6216 `last_message` \u4E3A\u4E3B\u3002",
|
|
117890
118119
|
"- \u4E0D\u8981\u4F9D\u8D56 bridge \u4ECE\u8F93\u51FA\u91CC\u89E3\u6790\u4E1A\u52A1\u9636\u6BB5\u3001MR\u3001\u9884\u89C8\u5730\u5740\u6216\u4E0B\u4E00\u6B65\u52A8\u4F5C;\u9700\u8981\u5C55\u793A\u7684\u5185\u5BB9\u4F60\u81EA\u5DF1\u5199\u8FDB last_message\u3002",
|
|
117891
118120
|
"- \u4E0D\u8981\u6C42\u56FA\u5B9A\u683C\u5F0F\u3002\u6839\u636E\u4EFB\u52A1\u9009\u62E9\u6700\u6E05\u695A\u7684\u8868\u8FBE:\u77ED\u7ED3\u8BBA\u3001\u5206\u70B9\u3001\u8868\u683C\u3001\u94FE\u63A5\u3001\u4E0B\u4E00\u6B65\u95EE\u9898\u90FD\u53EF\u4EE5\u3002",
|
|
117892
118121
|
"- \u5982\u679C\u672C\u8F6E\u6D89\u53CA repo / \u4EE3\u7801 / \u6587\u6863\u4FEE\u6539,last_message \u5E94\u5305\u542B\u8DB3\u591F\u8BA9\u8FD0\u8425\u9A8C\u6536\u7684\u8BC1\u636E,\u4F8B\u5982\u4F60\u5B9E\u9645\u4F7F\u7528\u7684 workspace/repo\u3001\u5173\u952E diff \u6216\u94FE\u63A5\u3001\u8FD0\u884C\u8FC7\u7684\u6D4B\u8BD5/\u68C0\u67E5\u547D\u4EE4\u548C\u7ED3\u679C\u3002\u5177\u4F53\u8BC1\u636E\u7531\u4EFB\u52A1\u51B3\u5B9A;dogfood E2E \u7684\u4E25\u683C\u6E05\u5355\u53EA\u5728 dogfood guide \u4E2D\u8981\u6C42\u3002",
|
|
@@ -117904,6 +118133,7 @@ function renderStateContract(stateFilePath) {
|
|
|
117904
118133
|
" \u6240\u4EE5\u4F60**\u4E0D\u8981\u5728 `last_message` \u91CC\u518D\u624B\u52A8\u5217\u4E00\u904D\u9009\u9879**(\u4F1A\u91CD\u590D);\u6B63\u6587\u53EA\u5199\u95EE\u9898\u80CC\u666F,\u9009\u9879\u4EA4\u7ED9 bridge \u6E32\u67D3",
|
|
117905
118134
|
"- \u628A `value` \u5199\u6210\u4E00\u53E5\u81EA\u63CF\u8FF0\u7684\u5B8C\u6574\u6307\u4EE4(\u5B83\u5C31\u662F\u4F60\u5C06\u6536\u5230\u7684\u4EFB\u52A1\u6587\u672C),\u4E0D\u8981\u5199\u6210 `optA` \u8FD9\u79CD\u4EE3\u53F7",
|
|
117906
118135
|
"- \u6CA1\u6709\u9700\u8981\u9009\u7684\u5C31**\u7701\u7565** `choices`(\u5361\u7247\u4FDD\u6301\u5E72\u51C0,\u65E0\u6309\u94AE)",
|
|
118136
|
+
"- \u82E5\u540C\u65F6\u5199 `content_blocks` \u548C `choices`,bridge \u59CB\u7EC8\u628A choices \u6E32\u67D3\u5728\u6B63\u6587\u5185\u5BB9\u4E4B\u540E;\u82E5 `status=failed`,error \u4ECD\u4F1A\u663E\u793A,\u4E0D\u4F1A\u88AB content_blocks \u8986\u76D6\u3002",
|
|
117907
118137
|
"",
|
|
117908
118138
|
"\u5199 status=ready \u524D\u5FC5\u987B\u81EA\u5DF1\u7528\u4EE3\u7801\u9A8C\u8FC7(dev server \u7528 curl -I \u770B 200;\u6587\u4EF6 ls/test -f;\u547D\u4EE4\u770B exit code)\u3002bridge \u4E0D\u518D\u66FF\u4F60 probe,\u9A8C\u8BC1\u5B8C\u5168\u662F\u4F60\u7684\u8D23\u4EFB\u3002",
|
|
117909
118139
|
"\u7B2C\u4E00\u4E2A\u5DE5\u5177\u5931\u8D25\u7ACB\u523B\u5199 status=failed + error,\u522B\u88F8\u5954\u649E\u591A\u4E2A\u5DE5\u5177\u5237\u6210\u8D85\u65F6;\u9700\u6C42\u6709\u6B67\u4E49\u5148\u505C\u4E0B\u5199 status=in_progress + last_message='\u7B49X\u786E\u8BA4',\u522B\u778E\u731C\u5C31\u5F00\u505A\u4E0D\u53EF\u9006\u52A8\u4F5C\u3002",
|
|
@@ -118681,6 +118911,48 @@ var optionalUrl = external_exports.preprocess(
|
|
|
118681
118911
|
(v) => typeof v === "string" && v.trim() === "" ? void 0 : v,
|
|
118682
118912
|
external_exports.string().optional()
|
|
118683
118913
|
);
|
|
118914
|
+
var imageAlt = external_exports.preprocess(
|
|
118915
|
+
(v) => typeof v === "string" && v.trim() === "" ? void 0 : v,
|
|
118916
|
+
external_exports.string().trim().min(1).max(200).default("\u56FE\u7247\u9884\u89C8")
|
|
118917
|
+
);
|
|
118918
|
+
var imageTitle = external_exports.preprocess(
|
|
118919
|
+
(v) => typeof v === "string" && v.trim() === "" ? void 0 : v,
|
|
118920
|
+
external_exports.string().trim().min(1).max(200).optional()
|
|
118921
|
+
);
|
|
118922
|
+
var ImageBlockSchema = external_exports.object({
|
|
118923
|
+
img_key: external_exports.string().trim().min(1).max(256),
|
|
118924
|
+
alt: imageAlt,
|
|
118925
|
+
title: imageTitle,
|
|
118926
|
+
/**
|
|
118927
|
+
* Agent-facing mode maps directly to Feishu Card JSON 2.0 `scale_type`.
|
|
118928
|
+
* Keep the enum narrow until we have live-card evidence for more modes.
|
|
118929
|
+
*/
|
|
118930
|
+
mode: external_exports.enum(["crop_center", "fit_horizontal"]).default("fit_horizontal"),
|
|
118931
|
+
preview: external_exports.boolean().default(true)
|
|
118932
|
+
});
|
|
118933
|
+
var ContentMarkdownBlockSchema = external_exports.object({
|
|
118934
|
+
type: external_exports.literal("markdown"),
|
|
118935
|
+
content: external_exports.string().trim().min(1).max(2e4)
|
|
118936
|
+
});
|
|
118937
|
+
var ContentImageBlockSchema = ImageBlockSchema.extend({
|
|
118938
|
+
type: external_exports.literal("image")
|
|
118939
|
+
});
|
|
118940
|
+
var ContentBlockSchema = external_exports.discriminatedUnion("type", [
|
|
118941
|
+
ContentMarkdownBlockSchema,
|
|
118942
|
+
ContentImageBlockSchema
|
|
118943
|
+
]);
|
|
118944
|
+
var ContentBlocksSchema = external_exports.array(ContentBlockSchema).max(12).superRefine((blocks, ctx) => {
|
|
118945
|
+
const imageCount = blocks.filter((b) => b.type === "image").length;
|
|
118946
|
+
if (imageCount > 4) {
|
|
118947
|
+
ctx.addIssue({
|
|
118948
|
+
code: external_exports.ZodIssueCode.too_big,
|
|
118949
|
+
type: "array",
|
|
118950
|
+
maximum: 4,
|
|
118951
|
+
inclusive: true,
|
|
118952
|
+
message: "content_blocks supports at most 4 image blocks"
|
|
118953
|
+
});
|
|
118954
|
+
}
|
|
118955
|
+
});
|
|
118684
118956
|
var StateFileSchema = external_exports.object({
|
|
118685
118957
|
// Thin channel: the bridge only validates `status`. Any other key the bot
|
|
118686
118958
|
// writes (incl. a legacy `stage`) is a business field — z.object STRIPS
|
|
@@ -118757,6 +119029,20 @@ var StateFileSchema = external_exports.object({
|
|
|
118757
119029
|
* V1 bots never write this field → V1 cards are byte-for-byte unchanged.
|
|
118758
119030
|
*/
|
|
118759
119031
|
choices: external_exports.array(external_exports.object({ label: external_exports.string().min(1), value: external_exports.string().min(1) })).max(5).optional(),
|
|
119032
|
+
/**
|
|
119033
|
+
* V2 image blocks (thin-channel). Agents upload or otherwise obtain `img_key`
|
|
119034
|
+
* themselves, then declare generic image previews here. Bridge only renders the
|
|
119035
|
+
* already-declared keys into Feishu Card JSON 2.0 image elements; it does not
|
|
119036
|
+
* download, upload, choose assets, or interpret platform workflows.
|
|
119037
|
+
*/
|
|
119038
|
+
image_blocks: external_exports.array(ImageBlockSchema).max(4).optional(),
|
|
119039
|
+
/**
|
|
119040
|
+
* V2 ordered card body blocks (thin-channel). When present and non-empty,
|
|
119041
|
+
* these are the authoritative card body sequence and take precedence over
|
|
119042
|
+
* legacy `last_message` + tail-appended `image_blocks` rendering. Narrow union
|
|
119043
|
+
* only: markdown and image. No raw card JSON escape hatch.
|
|
119044
|
+
*/
|
|
119045
|
+
content_blocks: ContentBlocksSchema.optional(),
|
|
118760
119046
|
/**
|
|
118761
119047
|
* Optional one-line prompt rendered above the choice buttons (e.g. "选哪个方案?").
|
|
118762
119048
|
* Rendered VERBATIM, bridge-opaque. Only meaningful alongside `choices`.
|
|
@@ -119566,7 +119852,9 @@ var BridgeHandler = class {
|
|
|
119566
119852
|
// reportedState is null when state.json wasn't freshly written
|
|
119567
119853
|
// (stale-guard), so stale leftover choices never reappear.
|
|
119568
119854
|
choices: reportedState?.choices,
|
|
119569
|
-
choicePrompt: reportedState?.choice_prompt
|
|
119855
|
+
choicePrompt: reportedState?.choice_prompt,
|
|
119856
|
+
imageBlocks: reportedState?.image_blocks,
|
|
119857
|
+
contentBlocks: reportedState?.content_blocks
|
|
119570
119858
|
});
|
|
119571
119859
|
await deleteCardFile(worktreePath);
|
|
119572
119860
|
}
|
|
@@ -123852,6 +124140,30 @@ function runtimeRequirementsForBots(bots) {
|
|
|
123852
124140
|
});
|
|
123853
124141
|
}
|
|
123854
124142
|
|
|
124143
|
+
// src/crashGuard.ts
|
|
124144
|
+
import { inspect } from "node:util";
|
|
124145
|
+
function handleUncaughtException(err, origin, log = console) {
|
|
124146
|
+
log.error(
|
|
124147
|
+
`[larkway] uncaughtException (origin=${origin}) \u2014 bridge STAYS UP (crash guard). Likely a transient WS handshake-timeout raw socket error; if it recurs, investigate:`,
|
|
124148
|
+
err
|
|
124149
|
+
);
|
|
124150
|
+
}
|
|
124151
|
+
function handleUnhandledRejection(reason, log = console) {
|
|
124152
|
+
const rendered = reason instanceof Error ? reason : inspect(reason, { depth: 4 });
|
|
124153
|
+
log.error(
|
|
124154
|
+
"[larkway] unhandledRejection \u2014 bridge STAYS UP (crash guard). Full reason:",
|
|
124155
|
+
rendered
|
|
124156
|
+
);
|
|
124157
|
+
}
|
|
124158
|
+
function registerCrashGuard(log = console) {
|
|
124159
|
+
process.on("uncaughtException", (err, origin) => {
|
|
124160
|
+
handleUncaughtException(err, origin, log);
|
|
124161
|
+
});
|
|
124162
|
+
process.on("unhandledRejection", (reason) => {
|
|
124163
|
+
handleUnhandledRejection(reason, log);
|
|
124164
|
+
});
|
|
124165
|
+
}
|
|
124166
|
+
|
|
123855
124167
|
// src/main.ts
|
|
123856
124168
|
var STATUS_WRITE_INTERVAL_MS = 3e4;
|
|
123857
124169
|
var VERSION = resolveLarkwayVersion(import.meta.url);
|
|
@@ -124115,6 +124427,7 @@ async function runV2Mode({
|
|
|
124115
124427
|
process.on("SIGTERM", () => {
|
|
124116
124428
|
void shutdown("SIGTERM");
|
|
124117
124429
|
});
|
|
124430
|
+
registerCrashGuard();
|
|
124118
124431
|
if (dryRun) {
|
|
124119
124432
|
console.log("[dry-run] V2 mode \u2014 all bots wired OK, exiting.");
|
|
124120
124433
|
await Promise.all(
|