openclaw-lark-multi-agent 1.8.19 → 1.8.20
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 +10 -0
- package/dist/feishu-bot.js +200 -86
- package/dist/input-status-display.d.ts +45 -0
- package/dist/input-status-display.js +67 -0
- package/dist/live-status.d.ts +22 -3
- package/dist/live-status.js +93 -25
- package/dist/message-store.d.ts +29 -1
- package/dist/message-store.js +168 -9
- package/dist/openclaw-client.d.ts +0 -2
- package/dist/openclaw-client.js +153 -55
- package/dist/queued-presentation.d.ts +8 -1
- package/dist/queued-presentation.js +109 -4
- package/dist/terminal-errors.d.ts +12 -0
- package/dist/terminal-errors.js +23 -3
- package/docs/releases/1.8.20.md +30 -0
- package/package.json +3 -2
package/dist/feishu-bot.d.ts
CHANGED
|
@@ -305,6 +305,11 @@ export declare class FeishuBot {
|
|
|
305
305
|
private deliverySourceId;
|
|
306
306
|
private sendSystemDelivery;
|
|
307
307
|
private enqueueAndDispatchDelivery;
|
|
308
|
+
private scopeRunError;
|
|
309
|
+
/** Shared original-error authorization for journaled terminals AND ordinary
|
|
310
|
+
* admitted provider errors. Current outbox/config metadata is not authority
|
|
311
|
+
* to replace the retained admission route or physical target. */
|
|
312
|
+
private errorRouteRevocation;
|
|
308
313
|
private inspectErrorNotice;
|
|
309
314
|
private deferErrorNotice;
|
|
310
315
|
private scheduleErrorNoticeCheck;
|
|
@@ -337,6 +342,8 @@ export declare class FeishuBot {
|
|
|
337
342
|
private fetchThinkingLabel;
|
|
338
343
|
private asDiscussionParticipant;
|
|
339
344
|
private runDiscussionTurn;
|
|
345
|
+
private liveInputElements;
|
|
346
|
+
private liveStatusRequest;
|
|
340
347
|
private buildLiveStatusCard;
|
|
341
348
|
/** Minimal escaping so activity text does not break card markdown. */
|
|
342
349
|
private escapeCardText;
|
|
@@ -502,5 +509,8 @@ export declare class FeishuBot {
|
|
|
502
509
|
/**
|
|
503
510
|
* Extract text content from a rich text (post) message.
|
|
504
511
|
*/
|
|
512
|
+
/** Visible rich-post fragments only. Text nodes remain literal; only actual
|
|
513
|
+
* media nodes become type markers, never a regex over the user's prose. */
|
|
514
|
+
private visiblePostInput;
|
|
505
515
|
private extractPostText;
|
|
506
516
|
}
|
package/dist/feishu-bot.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { INPUT_DISPLAY_LIMITS, LIVE_CARD_BYTES, displayClip, hintDetail, hintLabel, inertInputText, inputPreview, visibleInputPreview, selectInputHints } from "./input-status-display.js";
|
|
1
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
3
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
4
|
import * as lark from "@larksuiteoapi/node-sdk";
|
|
@@ -7,11 +8,11 @@ import { getI18n, normalizeLocale } from "./i18n.js";
|
|
|
7
8
|
import { InactiveRunObservation, AdmissionUnconfirmed, CollectorStalledError, FollowupAdmissionRefused } from "./openclaw-client.js";
|
|
8
9
|
import { SessionWaitPaused, isWaitTimeout, normalizeSessionRuntimeStatus, formatSessionFooter, FOREGROUND_WAIT_MS } from "./session-status.js";
|
|
9
10
|
import { SupersededObservation } from "./openclaw-client.js";
|
|
10
|
-
import { LiveStatusController, LiveStatusEffectRejectedError, LiveStatusEffectUnknownError } from "./live-status.js";
|
|
11
|
+
import { LiveStatusController, LiveStatusEffectRejectedError, LiveStatusEffectUnknownError, LiveStatusEffectSuppressedError } from "./live-status.js";
|
|
11
12
|
import { QueuedPresentations } from "./queued-presentation.js";
|
|
12
13
|
import { CompactProgressController } from "./compact-progress.js";
|
|
13
14
|
import { GoController } from "./go-controller.js";
|
|
14
|
-
import { errorIdentity } from "./terminal-errors.js";
|
|
15
|
+
import { RunTerminalError, closedStopReason, errorNoticeIdentity, errorIdentity } from "./terminal-errors.js";
|
|
15
16
|
import { formatProviderError, safeErrorSummary } from "./provider-errors.js";
|
|
16
17
|
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "fs";
|
|
17
18
|
import { basename, extname, join, resolve } from "path";
|
|
@@ -444,6 +445,10 @@ export class FeishuBot {
|
|
|
444
445
|
if (!rowId)
|
|
445
446
|
return;
|
|
446
447
|
this.store.clearPendingTrigger(this.config.name, chatId, rowId);
|
|
448
|
+
// Privacy invalidation starts before reaction SDK I/O: unrelated lookup
|
|
449
|
+
// latency must not retain a recalled preview on a frozen/terminal card.
|
|
450
|
+
void this.queuedPresentations.refreshRecalledInput(chatId, rowId, messageId)
|
|
451
|
+
.catch(err => console.warn(`[${this.config.name}] Recalled-input UI redaction unavailable:`, this.errorSummary(err)));
|
|
447
452
|
const pendingAcks = this.pendingAckMessages.get(chatId) || [];
|
|
448
453
|
const remainingAcks = [];
|
|
449
454
|
for (const ack of pendingAcks) {
|
|
@@ -561,7 +566,7 @@ export class FeishuBot {
|
|
|
561
566
|
}
|
|
562
567
|
if (meta?.sourceType === "run_error" || provenance) {
|
|
563
568
|
if (provenance) {
|
|
564
|
-
this.store.observeRunError(this.config.name, chatId, provenance);
|
|
569
|
+
this.store.observeRunError(this.config.name, chatId, this.scopeRunError(chatId, provenance));
|
|
565
570
|
await this.reconcileErrorNotices(chatId);
|
|
566
571
|
}
|
|
567
572
|
else
|
|
@@ -915,7 +920,7 @@ export class FeishuBot {
|
|
|
915
920
|
const drainTasks = [];
|
|
916
921
|
for (const chat of chats) {
|
|
917
922
|
// Skip p2p chats that belong to other bots
|
|
918
|
-
if (chat.chatType === "p2p" && chat.
|
|
923
|
+
if (chat.chatType === "p2p" && !this.store.claimP2pOwner(chat.chatId, this.config.name, this.config.appId)) {
|
|
919
924
|
continue;
|
|
920
925
|
}
|
|
921
926
|
// Re-subscribe to existing sessions
|
|
@@ -973,7 +978,9 @@ export class FeishuBot {
|
|
|
973
978
|
if ((message.mentions || []).some((mention) => mention?.id && Object.hasOwn(mention.id, 'open_id') &&
|
|
974
979
|
(typeof mention.id.open_id !== 'string' || !mention.id.open_id.trim())))
|
|
975
980
|
return;
|
|
976
|
-
|
|
981
|
+
// Private duplicates must reach the stable app/chat fence so an old
|
|
982
|
+
// name-scoped tombstone cannot disappear across a later bot rename.
|
|
983
|
+
if (message.chat_type !== "p2p" && this.store.hasBotProcessed(this.config.name, message.message_id))
|
|
977
984
|
return;
|
|
978
985
|
// A duplicate must honor the original journal owner, even if its envelope
|
|
979
986
|
// differs or identity has resolved since the first receipt.
|
|
@@ -1031,7 +1038,9 @@ export class FeishuBot {
|
|
|
1031
1038
|
// OpenClaw twice.
|
|
1032
1039
|
if (this.identityClosed)
|
|
1033
1040
|
return;
|
|
1034
|
-
if (!identityItem && !
|
|
1041
|
+
if (!identityItem && !(chatType === "p2p"
|
|
1042
|
+
? this.store.tryMarkP2pProcessed(this.config.appId, chatId, this.config.name, messageId)
|
|
1043
|
+
: this.store.tryMarkBotProcessed(this.config.name, messageId)))
|
|
1035
1044
|
return;
|
|
1036
1045
|
const identityCurrent = () => {
|
|
1037
1046
|
if (this.identityClosed)
|
|
@@ -1051,11 +1060,8 @@ export class FeishuBot {
|
|
|
1051
1060
|
if (!identityCurrent())
|
|
1052
1061
|
return;
|
|
1053
1062
|
// --- P2P isolation: only the owning bot processes p2p messages ---
|
|
1054
|
-
if (chatType === "p2p") {
|
|
1055
|
-
|
|
1056
|
-
if (chatInfo?.ownerBot && chatInfo.ownerBot !== this.config.name) {
|
|
1057
|
-
return; // This p2p chat belongs to another bot
|
|
1058
|
-
}
|
|
1063
|
+
if (chatType === "p2p" && !this.store.claimP2pOwner(chatId, this.config.name, this.config.appId)) {
|
|
1064
|
+
return; // A display-name match cannot override a conflicting app pin.
|
|
1059
1065
|
}
|
|
1060
1066
|
let content;
|
|
1061
1067
|
try {
|
|
@@ -1065,6 +1071,14 @@ export class FeishuBot {
|
|
|
1065
1071
|
console.warn(`[${this.config.name}] Failed to parse message content, skipping`);
|
|
1066
1072
|
return;
|
|
1067
1073
|
}
|
|
1074
|
+
// Original visible input for UI only; never use hydrated media paths,
|
|
1075
|
+
// synthetic routing prompts or model/history text as its body.
|
|
1076
|
+
const inputCaption = [content.text, content.caption].find(value => typeof value === 'string') || '';
|
|
1077
|
+
const inputDisplayText = messageType === 'text' ? (typeof content.text === 'string' ? content.text.replace(/@_user_\d+/g, (key) => { const mention = (message.mentions || []).find((item) => item.key === key); return typeof mention?.name === 'string' ? `@${mention.name}` : key; }) : '') :
|
|
1078
|
+
messageType === 'file' ? `[${this.isEn(chatId) ? 'File' : '文件'}: ${(typeof content.file_name === 'string' ? content.file_name : 'file').split(/[\\/]/).at(-1)}] ${inputCaption}` :
|
|
1079
|
+
messageType === 'image' ? `[${this.isEn(chatId) ? 'Image' : '图片'}] ${inputCaption}` : messageType === 'audio' ? `[${this.isEn(chatId) ? 'Audio' : '语音'}] ${inputCaption}` : messageType === 'sticker' ? (this.isEn(chatId) ? '[Sticker]' : '[贴纸]') :
|
|
1080
|
+
messageType === 'post' ? this.visiblePostInput(content, chatId) :
|
|
1081
|
+
[content.title, content.name, content.text, content.url].find(value => typeof value === 'string') || '';
|
|
1068
1082
|
// --- Extract text content based on message type ---
|
|
1069
1083
|
let cleanText = "";
|
|
1070
1084
|
let rawText = "";
|
|
@@ -1622,7 +1636,7 @@ export class FeishuBot {
|
|
|
1622
1636
|
// No health/setup/model/history await precedes this honest local receipt.
|
|
1623
1637
|
// UI custody cannot reserve dispatch or claim Gateway/model acceptance.
|
|
1624
1638
|
const presentationRoot = this.queueOwners.get(chatId)?.runId || this.store.getDispatchCustody(this.config.name, chatId)?.runId;
|
|
1625
|
-
this.presentation(() => this.queuedPresentations.receive(chatId, insertedId, messageId, isNativeOpenClawCommand, presentationRoot));
|
|
1639
|
+
this.presentation(() => this.queuedPresentations.receive(chatId, insertedId, messageId, isNativeOpenClawCommand, presentationRoot, { source: 'visible', text: inputDisplayText }));
|
|
1626
1640
|
// Reserve BEFORE the first health/reaction await. Otherwise an ending
|
|
1627
1641
|
// owner can ordinary-drain this row while its handler is still waiting,
|
|
1628
1642
|
// and the later steer would submit the same message a second time.
|
|
@@ -2739,7 +2753,7 @@ export class FeishuBot {
|
|
|
2739
2753
|
owner.continuePending = mergedTriggers.some(message => !identityTargets.some(entry => entry.message === message));
|
|
2740
2754
|
break;
|
|
2741
2755
|
}
|
|
2742
|
-
await liveStatus?.pauseForUnconfirmedResult(this.isEn(chatId) ? "Session setup failed before admission" : "会话准备失败,尚未提交 Gateway", this.isEn(chatId) ? `${this.config.name} setup unavailable` : `${this.config.name}
|
|
2756
|
+
await liveStatus?.pauseForUnconfirmedResult(this.isEn(chatId) ? "Session setup failed before admission" : "会话准备失败,尚未提交 Gateway", this.isEn(chatId) ? `${this.config.name} setup unavailable` : `${this.config.name} 会话准备未完成`, "blocked").catch(() => { });
|
|
2743
2757
|
if (batchProof) {
|
|
2744
2758
|
for (const id of mergedTriggerIds) {
|
|
2745
2759
|
const item = this.store.getInsertion(this.config.name, chatId, id);
|
|
@@ -3029,7 +3043,7 @@ export class FeishuBot {
|
|
|
3029
3043
|
const outcome = await this.enqueueAndDispatchDelivery(chatId, "assistant_visible", this.deliverySourceId("visible", `${triggerId}|${(shouldReply ? visibleReply : "").trim()}|${JSON.stringify(parsedReply.attachments)}`), shouldReply ? visibleReply : "", parsedReply.attachments, lastHuman.messageId, `trigger:${triggerId}`, this.config.model, foregroundRunId ? { runId: foregroundRunId, sessionKey, ...(foregroundSessionId ? { sessionId: foregroundSessionId } : {}), final: true } : undefined, undefined, mergedTriggerIds);
|
|
3030
3044
|
visibleOutcomeConfirmed = outcome.state === 'delivered';
|
|
3031
3045
|
if (!visibleOutcomeConfirmed) {
|
|
3032
|
-
await liveStatus?.pauseForUnconfirmedResult("飞书投递回执尚未确认;保留原投递保管,不重复发送").catch(() => { });
|
|
3046
|
+
await liveStatus?.pauseForUnconfirmedResult("飞书投递回执尚未确认;保留原投递保管,不重复发送", undefined, "delivery_pending").catch(() => { });
|
|
3033
3047
|
}
|
|
3034
3048
|
else {
|
|
3035
3049
|
await liveStatus?.complete();
|
|
@@ -3151,17 +3165,17 @@ export class FeishuBot {
|
|
|
3151
3165
|
this.store.saveInsertion(this.config.name, chatId, id, row.insertionId, 'target_changed', { ...row, attempted: false, reason: err.message });
|
|
3152
3166
|
}
|
|
3153
3167
|
await this.enqueueAndDispatchDelivery(chatId, 'admission_status', `admission:${err.submissionId}:refused`, err.message, [], lastHuman.messageId, `admission:${err.submissionId}:refused`).catch(() => { });
|
|
3154
|
-
await liveStatus?.pauseForUnconfirmedResult(err.message).catch(() => { });
|
|
3168
|
+
await liveStatus?.pauseForUnconfirmedResult(err.message, undefined, "blocked").catch(() => { });
|
|
3155
3169
|
owner.continuePending = true; // independently proved newer cohorts may still be sent
|
|
3156
3170
|
break;
|
|
3157
3171
|
}
|
|
3158
3172
|
if (err instanceof AdmissionUnconfirmed) {
|
|
3159
3173
|
await this.enqueueAndDispatchDelivery(chatId, 'admission_status', `admission:${err.submissionId}:unknown`, err.message, [], lastHuman.messageId, `admission:${err.submissionId}:unknown`).catch(() => { });
|
|
3160
|
-
await liveStatus?.pauseForUnconfirmedResult(err.message).catch(() => { });
|
|
3174
|
+
await liveStatus?.pauseForUnconfirmedResult(err.message, undefined, "submitting").catch(() => { });
|
|
3161
3175
|
break;
|
|
3162
3176
|
}
|
|
3163
3177
|
let pause = err instanceof SessionWaitPaused ? err : undefined;
|
|
3164
|
-
if (!pause && isWaitTimeout(err instanceof Error ? err.message : String(err))) {
|
|
3178
|
+
if (!pause && !(err instanceof RunTerminalError) && isWaitTimeout(err instanceof Error ? err.message : String(err))) {
|
|
3165
3179
|
const state = await this.readSessionStatus(chatId);
|
|
3166
3180
|
if (state.running)
|
|
3167
3181
|
pause = new SessionWaitPaused(state, true);
|
|
@@ -3170,7 +3184,7 @@ export class FeishuBot {
|
|
|
3170
3184
|
const target = this.activeDeliveryTargets.get(chatId);
|
|
3171
3185
|
if (target?.triggerId === triggerId && target.runId)
|
|
3172
3186
|
this.openclawClient.releasePausedWait?.(target.runId);
|
|
3173
|
-
await liveStatus?.pauseForUnconfirmedResult(pause.message, `${this.config.name} 等待暂停 · ${pause.snapshot.status}
|
|
3187
|
+
await liveStatus?.pauseForUnconfirmedResult(pause.message, `${this.config.name} 等待暂停 · ${pause.snapshot.status}`, "paused").catch(() => { });
|
|
3174
3188
|
await this.sendWaitPaused(chatId, triggerId, lastHuman.messageId, pause).catch(() => { });
|
|
3175
3189
|
// A wait notice is not the final answer and must never claim its key.
|
|
3176
3190
|
// Do not mark DONE, retry this input, or abort the running session.
|
|
@@ -3226,7 +3240,7 @@ export class FeishuBot {
|
|
|
3226
3240
|
if (row && !row.admissionSubmissionId && !row.admittedRunId)
|
|
3227
3241
|
this.store.saveInsertion(this.config.name, chatId, id, row.insertionId, 'target_changed', { ...row, attempted: false, reason: 'guarded pre-admission failure; input retained without sync or new-ID retry' });
|
|
3228
3242
|
}
|
|
3229
|
-
await liveStatus?.pauseForUnconfirmedResult('Guarded admission unavailable; input custody retained without replay').catch(() => { });
|
|
3243
|
+
await liveStatus?.pauseForUnconfirmedResult('Guarded admission unavailable; input custody retained without replay', undefined, 'blocked').catch(() => { });
|
|
3230
3244
|
break;
|
|
3231
3245
|
}
|
|
3232
3246
|
console.error(`[${this.config.name}] processQueue error:`, safeErrorSummary(err));
|
|
@@ -3236,13 +3250,23 @@ export class FeishuBot {
|
|
|
3236
3250
|
if (lastHuman.messageId) {
|
|
3237
3251
|
// Error message goes through the normal delivery path; the live status
|
|
3238
3252
|
// is a separate message that we mark interrupted afterwards.
|
|
3239
|
-
|
|
3240
|
-
.
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
}
|
|
3245
|
-
|
|
3253
|
+
if (err instanceof RunTerminalError) {
|
|
3254
|
+
const p = this.scopeRunError(chatId, err.provenance);
|
|
3255
|
+
this.store.observeRunError(this.config.name, chatId, p);
|
|
3256
|
+
await this.reconcileErrorNotices(chatId);
|
|
3257
|
+
errorOutcomeConfirmed = this.store.findRunErrorOwner(this.config.name, chatId, p)?.status === 'delivered';
|
|
3258
|
+
}
|
|
3259
|
+
else
|
|
3260
|
+
await this.enqueueAndDispatchDelivery(chatId, "provider_error", `trigger:${triggerId}:provider-error`, errorText, [], lastHuman.messageId, `trigger:${triggerId}:provider-error`, undefined, foregroundRunId ? (() => {
|
|
3261
|
+
const p = this.scopeRunError(chatId, { sessionKey: `agent:main:${this.getSessionKey(chatId).replace(/^agent:[^:]+:/, '')}`, sessionId: owner.sessionId, runId: foregroundRunId, source: 'chat', state: 'error', detail: err instanceof Error ? err.message : undefined });
|
|
3262
|
+
return { sessionKey: p.sessionKey, sessionId: p.sessionId, runId: p.runId, error: p };
|
|
3263
|
+
})() : undefined)
|
|
3264
|
+
.then(outcome => {
|
|
3265
|
+
errorOutcomeConfirmed = outcome.state === 'delivered';
|
|
3266
|
+
if (errorOutcomeConfirmed && current() && triggerId)
|
|
3267
|
+
this.store.markDeliveredReply(this.config.name, chatId, triggerId, lastHuman.messageId);
|
|
3268
|
+
})
|
|
3269
|
+
.catch(() => { });
|
|
3246
3270
|
await liveStatus?.fail().catch(() => { });
|
|
3247
3271
|
}
|
|
3248
3272
|
else {
|
|
@@ -3485,6 +3509,7 @@ export class FeishuBot {
|
|
|
3485
3509
|
thinkingLevel: meta.thinkingLevel,
|
|
3486
3510
|
toolCalls: meta.toolCalls,
|
|
3487
3511
|
noReply: Boolean(meta.noReply),
|
|
3512
|
+
inputStatus: meta.inputStatus ? { ...meta.inputStatus, items: meta.inputStatus.items.map(item => item.kind === 'delivery_pending' ? { ...item, kind: 'completed' } : item) } : { items: [{ kind: meta.noReply ? "no_reply" : "completed", at: 0 }] },
|
|
3488
3513
|
state: "done",
|
|
3489
3514
|
};
|
|
3490
3515
|
await this.patchLiveStatusCard(messageId, view, chatId);
|
|
@@ -3779,7 +3804,12 @@ export class FeishuBot {
|
|
|
3779
3804
|
const previous = this.deliveryTargetsByRun.get(key);
|
|
3780
3805
|
if (previous?.timer)
|
|
3781
3806
|
clearTimeout(previous.timer);
|
|
3782
|
-
|
|
3807
|
+
const persisted = this.store.getDeliveryRunTarget(this.config.name, chatId, runId);
|
|
3808
|
+
const owner = this.queueOwners.get(chatId);
|
|
3809
|
+
this.store.bindDeliveryRun(this.config.name, chatId, runId, { ...persisted, triggerId: target.triggerId, messageId: target.messageId, runId,
|
|
3810
|
+
sessionId: persisted?.sessionId || (owner?.runId === runId ? owner.sessionId : undefined),
|
|
3811
|
+
botIdentity: persisted?.botIdentity || this.config.appId, stopGeneration: persisted?.stopGeneration ?? this.stopGeneration(chatId),
|
|
3812
|
+
sessionKey: persisted?.sessionKey || `agent:main:${this.getSessionKey(chatId).replace(/^agent:[^:]+:/, '')}` });
|
|
3783
3813
|
this.deliveryTargetsByRun.set(key, {
|
|
3784
3814
|
triggerId: target.triggerId,
|
|
3785
3815
|
messageId: target.messageId,
|
|
@@ -3878,9 +3908,9 @@ export class FeishuBot {
|
|
|
3878
3908
|
const contentHash = this.stableHash(normalizedPayload);
|
|
3879
3909
|
const finalDeliveryKey = deliveryKey || sourceId;
|
|
3880
3910
|
const deliveryMetaJson = JSON.stringify({
|
|
3881
|
-
botIdentity: this.config.appId,
|
|
3911
|
+
botIdentity: provenance?.error?.botIdentity || this.config.appId,
|
|
3882
3912
|
...(completionTriggerIds?.length ? { completionTriggerIds } : {}),
|
|
3883
|
-
deliveryStopEpoch: this.stopGeneration(chatId),
|
|
3913
|
+
deliveryStopEpoch: provenance?.error?.stopEpoch ?? this.stopGeneration(chatId),
|
|
3884
3914
|
...(finalReplyModel?.trim() ? { model: finalReplyModel.trim() } : {}),
|
|
3885
3915
|
...(provenance ? { provenance } : {}), ...(errorNoticeId ? { errorNoticeId } : {}),
|
|
3886
3916
|
});
|
|
@@ -4035,13 +4065,52 @@ export class FeishuBot {
|
|
|
4035
4065
|
}
|
|
4036
4066
|
return this.store.getDeliveryDispatchOutcome(this.config.name, chatId, finalDeliveryKey);
|
|
4037
4067
|
}
|
|
4068
|
+
scopeRunError(chatId, p) {
|
|
4069
|
+
const key = /^agent:[^:]+:/.test(p.sessionKey) ? p.sessionKey : `agent:main:${p.sessionKey}`;
|
|
4070
|
+
const target = this.store.getDeliveryRunTarget(this.config.name, chatId, p.runId);
|
|
4071
|
+
const observation = this.store.getRunObservation(key, p.runId);
|
|
4072
|
+
// Pre-upgrade targets may lack app/physical fields. A known exact legacy
|
|
4073
|
+
// outbox owner is original route evidence, never permission for the current
|
|
4074
|
+
// replacement app to take over its notice.
|
|
4075
|
+
const legacy = !target?.botIdentity ? this.store.findRunErrorOwner(this.config.name, chatId, { ...p, sessionKey: key }) : undefined;
|
|
4076
|
+
let meta;
|
|
4077
|
+
try {
|
|
4078
|
+
meta = JSON.parse(legacy?.deliveryMetaJson || '{}');
|
|
4079
|
+
}
|
|
4080
|
+
catch {
|
|
4081
|
+
meta = {};
|
|
4082
|
+
}
|
|
4083
|
+
return { ...p, sessionKey: key, sessionId: p.sessionId || target?.sessionId || observation?.sessionId || meta.provenance?.sessionId,
|
|
4084
|
+
botIdentity: p.botIdentity || target?.botIdentity || meta.botIdentity || this.config.appId,
|
|
4085
|
+
stopEpoch: p.stopEpoch ?? target?.stopGeneration ?? meta.deliveryStopEpoch ?? this.stopGeneration(chatId) };
|
|
4086
|
+
}
|
|
4087
|
+
/** Shared original-error authorization for journaled terminals AND ordinary
|
|
4088
|
+
* admitted provider errors. Current outbox/config metadata is not authority
|
|
4089
|
+
* to replace the retained admission route or physical target. */
|
|
4090
|
+
errorRouteRevocation(chatId, p) {
|
|
4091
|
+
const original = this.store.getDeliveryRunTarget(this.config.name, chatId, p.runId);
|
|
4092
|
+
if (original?.messageId && this.store.isMessageRecalled(original.messageId) ||
|
|
4093
|
+
original?.sessionId && p.sessionId && original.sessionId !== p.sessionId ||
|
|
4094
|
+
original?.sessionKey && canonicalPhysicalSessionKey(original.sessionKey) !== canonicalPhysicalSessionKey(p.sessionKey))
|
|
4095
|
+
return 'original_error_target_revoked';
|
|
4096
|
+
const epoch = this.stopGeneration(chatId);
|
|
4097
|
+
if (p.botIdentity && p.botIdentity !== this.config.appId || original?.botIdentity && original.botIdentity !== this.config.appId ||
|
|
4098
|
+
p.stopEpoch !== undefined && p.stopEpoch !== epoch || original?.stopGeneration !== undefined && original.stopGeneration !== epoch)
|
|
4099
|
+
return 'original_error_route_revoked';
|
|
4100
|
+
}
|
|
4038
4101
|
async inspectErrorNotice(record) {
|
|
4102
|
+
const revoked = this.errorRouteRevocation(record.chatId, record.provenance);
|
|
4103
|
+
if (revoked)
|
|
4104
|
+
return { state: 'stopped', reason: revoked };
|
|
4039
4105
|
if (this.store.hasRunFinalDelivery(this.config.name, record.chatId, record.provenance, true)) {
|
|
4040
4106
|
return { state: "suppressed", reason: "exact_run_final_platform_delivery_confirmed" };
|
|
4041
4107
|
}
|
|
4042
4108
|
if (this.store.hasRunFinalDelivery(this.config.name, record.chatId, record.provenance, false)) {
|
|
4043
4109
|
return { state: "pending", reason: "exact_run_final_delivery_pending" };
|
|
4044
4110
|
}
|
|
4111
|
+
const existing = this.store.findRunErrorOwner(this.config.name, record.chatId, record.provenance);
|
|
4112
|
+
if (!record.provenance.runId.startsWith('announce:') && existing && existing.deliveryKey !== `run-error:${errorNoticeIdentity(record.provenance)}`)
|
|
4113
|
+
return { state: 'suppressed', reason: 'existing_exact_run_error_owner' };
|
|
4045
4114
|
let verdict;
|
|
4046
4115
|
try {
|
|
4047
4116
|
verdict = await this.openclawClient.inspectRunError(record.provenance);
|
|
@@ -4051,8 +4120,13 @@ export class FeishuBot {
|
|
|
4051
4120
|
}
|
|
4052
4121
|
// Presentation only: leave exact-run finality, stop suppression and durable
|
|
4053
4122
|
// outbox keys owned by the existing verdict; no retry/model fallback here.
|
|
4054
|
-
if (verdict.state === "terminal" && verdict.reason
|
|
4055
|
-
|
|
4123
|
+
if (verdict.state === "terminal" && ['ordinary_gateway_error', 'closed_gateway_terminal', 'exact_run_terminal_error'].includes(verdict.reason)) {
|
|
4124
|
+
const reason = closedStopReason(record.provenance.stopReason) || verdict.stopReason;
|
|
4125
|
+
const en = this.isEn(record.chatId);
|
|
4126
|
+
verdict = { ...verdict, text: reason === 'timeout' ? this.formatUserVisibleError({ code: 'execution_timeout' }, record.chatId)
|
|
4127
|
+
: reason === 'superseded' ? (en ? '⚠️ This run ended after being superseded by a newer session writer; no user stop is inferred.' : '⚠️ 本次运行已被新的会话写入者取代而终止;不据此认定用户停止。')
|
|
4128
|
+
: reason === 'restart' ? (en ? '⚠️ This run ended for a Gateway restart; no user stop is inferred.' : '⚠️ 本次运行因 Gateway 重启而终止;不据此认定用户停止。')
|
|
4129
|
+
: this.formatUserVisibleError(record.provenance.detail, record.chatId) };
|
|
4056
4130
|
}
|
|
4057
4131
|
// A stop or final may have arrived during the read-only lookup.
|
|
4058
4132
|
const latest = this.store.getErrorNotice(this.config.name, record.chatId, record.noticeId);
|
|
@@ -4071,6 +4145,8 @@ export class FeishuBot {
|
|
|
4071
4145
|
if (this.store.hasRunFinalDelivery(this.config.name, record.chatId, record.provenance, false)) {
|
|
4072
4146
|
return { state: "pending", reason: "exact_run_final_delivery_pending" };
|
|
4073
4147
|
}
|
|
4148
|
+
if (verdict.state === 'terminal' && verdict.stopReason && !record.provenance.stopReason)
|
|
4149
|
+
record.provenance = { ...record.provenance, stopReason: verdict.stopReason };
|
|
4074
4150
|
return verdict;
|
|
4075
4151
|
}
|
|
4076
4152
|
deferErrorNotice(record, verdict) {
|
|
@@ -4153,7 +4229,7 @@ export class FeishuBot {
|
|
|
4153
4229
|
this.store.saveErrorNotice(record);
|
|
4154
4230
|
if (verdict.state === "terminal" && verdict.terminalKey && verdict.text) {
|
|
4155
4231
|
// Independent identity: never take the result's trigger/final key.
|
|
4156
|
-
await this.enqueueAndDispatchDelivery(chatId, "run_error", verdict.terminalKey, verdict.text, [], undefined, verdict.terminalKey, undefined, { sourceType: "run_error", sessionKey: record.provenance.sessionKey, runId: record.provenance.runId, error: record.provenance, terminalEvidence: verdict }, record.noticeId);
|
|
4232
|
+
await this.enqueueAndDispatchDelivery(chatId, "run_error", verdict.terminalKey, verdict.text, [], undefined, verdict.terminalKey, undefined, { sourceType: "run_error", sessionKey: record.provenance.sessionKey, sessionId: record.provenance.sessionId, runId: record.provenance.runId, error: record.provenance, terminalEvidence: verdict }, record.noticeId);
|
|
4157
4233
|
// The send-time recheck may have parked or suppressed the observation.
|
|
4158
4234
|
const refreshed = this.store.getErrorNotice(this.config.name, chatId, record.noticeId);
|
|
4159
4235
|
if (refreshed?.verdict && ["pending", "suppressed", "stopped"].includes(refreshed.verdict.state))
|
|
@@ -4326,8 +4402,13 @@ export class FeishuBot {
|
|
|
4326
4402
|
const meta = JSON.parse(item.deliveryMetaJson || '{}'), p = meta.provenance;
|
|
4327
4403
|
if (!this.insertionNoticeCurrent(chatId, meta.insertionNotice))
|
|
4328
4404
|
throw Object.assign(new Error('Insertion notice no longer current'), { deliveryStopped: true });
|
|
4329
|
-
if (
|
|
4330
|
-
|
|
4405
|
+
if (p?.error) {
|
|
4406
|
+
const revoked = this.errorRouteRevocation(chatId, p.error);
|
|
4407
|
+
if (revoked)
|
|
4408
|
+
throw Object.assign(new Error(`Original error delivery revoked: ${revoked}`), { deliveryStopped: true });
|
|
4409
|
+
}
|
|
4410
|
+
if ((p?.final || p?.error || item.sourceType === 'run_error') &&
|
|
4411
|
+
(meta.botIdentity && meta.botIdentity !== this.config.appId || typeof meta.deliveryStopEpoch === 'number' && meta.deliveryStopEpoch !== this.stopGeneration(chatId) ||
|
|
4331
4412
|
p?.sessionKey && p.runId && this.store.isStoppedObservation(p.sessionKey, p.runId))) {
|
|
4332
4413
|
throw Object.assign(new Error('Delivery revoked by explicit stop before platform attempt'), { deliveryStopped: true });
|
|
4333
4414
|
}
|
|
@@ -4699,7 +4780,7 @@ export class FeishuBot {
|
|
|
4699
4780
|
}
|
|
4700
4781
|
if (pause) {
|
|
4701
4782
|
discussionWaitPaused = true;
|
|
4702
|
-
await liveStatus?.pauseForUnconfirmedResult(pause.message, `${this.config.name} 等待暂停 · ${pause.snapshot.status}
|
|
4783
|
+
await liveStatus?.pauseForUnconfirmedResult(pause.message, `${this.config.name} 等待暂停 · ${pause.snapshot.status}`, "paused").catch(() => { });
|
|
4703
4784
|
await this.sendWaitPaused(chatId, 0, undefined, pause).catch(() => { });
|
|
4704
4785
|
}
|
|
4705
4786
|
else
|
|
@@ -4735,7 +4816,7 @@ export class FeishuBot {
|
|
|
4735
4816
|
const outcome = await this.enqueueAndDispatchDelivery(chatId, "discussion", `discussion:${Date.now()}:${Math.random().toString(36).slice(2)}`, isVisible ? displayReply : "", parsedReply.attachments, undefined, undefined, this.config.model);
|
|
4736
4817
|
discussionDeliveryConfirmed = outcome.state === 'delivered';
|
|
4737
4818
|
if (!discussionDeliveryConfirmed)
|
|
4738
|
-
await liveStatus?.pauseForUnconfirmedResult("飞书投递回执尚未确认;原投递仍受保管").catch(() => { });
|
|
4819
|
+
await liveStatus?.pauseForUnconfirmedResult("飞书投递回执尚未确认;原投递仍受保管", undefined, "delivery_pending").catch(() => { });
|
|
4739
4820
|
}
|
|
4740
4821
|
catch (err) {
|
|
4741
4822
|
await liveStatus?.fail().catch(() => { });
|
|
@@ -4755,13 +4836,46 @@ export class FeishuBot {
|
|
|
4755
4836
|
}
|
|
4756
4837
|
return { botName: this.config.name, text: cleanVisibleReply, visible: isVisible && discussionDeliveryConfirmed };
|
|
4757
4838
|
}
|
|
4758
|
-
|
|
4839
|
+
liveInputElements(view, chatId, limits) {
|
|
4840
|
+
const context = view.inputStatus;
|
|
4841
|
+
if (!context)
|
|
4842
|
+
return [];
|
|
4843
|
+
const en = this.isEn(chatId);
|
|
4844
|
+
const scoped = !!chatId && context.chatId === chatId && context.botName === this.config.name && context.appId === this.config.appId &&
|
|
4845
|
+
context.stopEpoch === this.stopGeneration(chatId);
|
|
4846
|
+
const preview = (hint) => {
|
|
4847
|
+
const source = scoped && hint.rowId ? this.store.getMessageByRowId(hint.rowId) : undefined;
|
|
4848
|
+
if (!source || source.chatId !== chatId || source.senderType !== 'human' || !hint.messageId || source.messageId !== hint.messageId)
|
|
4849
|
+
return en ? '[Message body unavailable]' : '[消息正文暂不可用]';
|
|
4850
|
+
if (this.store.isMessageRecalled(source.messageId))
|
|
4851
|
+
return en ? '[Message recalled]' : '[消息已撤回]';
|
|
4852
|
+
return hint.previewSource === 'visible' ? visibleInputPreview(hint.preview ?? '', limits.preview, en) : inputPreview(hint.preview ?? source.content, limits.preview, en);
|
|
4853
|
+
};
|
|
4854
|
+
const selected = selectInputHints(context.items, limits.members);
|
|
4855
|
+
const stamp = (at) => { const sec = Number.isFinite(at) ? Math.max(0, Math.floor(at)) : 0; return `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}`; };
|
|
4856
|
+
const lines = selected.map(hint => `${stamp(hint.at)} ${hintLabel(hint.kind, en)} ${inertInputText(preview(hint))}${hintDetail(hint.kind, en) ? '\n ↳ ' + inertInputText(hintDetail(hint.kind, en)) : ''}`);
|
|
4857
|
+
if (context.items.length > selected.length)
|
|
4858
|
+
lines.splice(Math.min(1, lines.length), 0, en ? `… ${context.items.length - selected.length} other inputs omitted; newest inputs retained` : `… 另有 ${context.items.length - selected.length} 条输入未展开,保留最新输入`);
|
|
4859
|
+
if (context.note)
|
|
4860
|
+
lines.push(`${stamp(context.note.at)} ${hintLabel(context.note.kind, en)} ${selected.map(hint => inertInputText(preview(hint))).join(' · ')}\n ↳ ${inertInputText(hintDetail(context.note.kind, en))}`);
|
|
4861
|
+
return lines.length ? [{ tag: 'markdown', content: lines.join('\n') }] : [];
|
|
4862
|
+
}
|
|
4863
|
+
liveStatusRequest(view, chatId, request) {
|
|
4864
|
+
for (const limits of INPUT_DISPLAY_LIMITS) {
|
|
4865
|
+
const value = request(this.buildLiveStatusCard(view, chatId, limits));
|
|
4866
|
+
if (Buffer.byteLength(JSON.stringify(value), 'utf8') <= LIVE_CARD_BYTES - 512)
|
|
4867
|
+
return value; // reserve SDK envelope overhead
|
|
4868
|
+
}
|
|
4869
|
+
throw new LiveStatusEffectSuppressedError('Live status request exceeds local outer JSON byte budget; no SDK call');
|
|
4870
|
+
}
|
|
4871
|
+
buildLiveStatusCard(view, chatId, limits = INPUT_DISPLAY_LIMITS[0]) {
|
|
4759
4872
|
const en = this.isEn(chatId);
|
|
4873
|
+
const inputElements = this.liveInputElements(view, chatId, limits);
|
|
4760
4874
|
if (view.state === "replied") {
|
|
4761
4875
|
// Neutral UI-only closure, not a green business/input-completion claim.
|
|
4762
4876
|
const summary = en ? `💬 Linked run replied · ⏱ ${view.elapsed}` : `💬 关联轮次已回复 · ⏱ 耗时${view.elapsed}`;
|
|
4763
4877
|
return { schema: "2.0", config: { update_multi: true, width_mode: "fill" },
|
|
4764
|
-
body: { elements: [{ tag: "markdown", content: `<font color='grey'>${summary}</font>` }] } };
|
|
4878
|
+
body: { elements: [...inputElements, { tag: "markdown", content: `<font color='grey'>${summary}</font>` }] } };
|
|
4765
4879
|
}
|
|
4766
4880
|
// Clean finish (done / NO_REPLY): keep it minimal — no header, no footer, just
|
|
4767
4881
|
// one compact grey line (status emoji + tool-call count + total elapsed) so
|
|
@@ -4770,18 +4884,18 @@ export class FeishuBot {
|
|
|
4770
4884
|
const statusEmoji = view.noReply ? "💤" : "✅";
|
|
4771
4885
|
const summary = this.completionSummary({
|
|
4772
4886
|
toolCalls: view.toolCalls,
|
|
4773
|
-
elapsed: view.elapsed,
|
|
4774
|
-
model: view.model,
|
|
4775
|
-
thinkingLevel: view.thinkingLevel,
|
|
4887
|
+
elapsed: displayClip(view.elapsed, 32),
|
|
4888
|
+
model: view.model ? displayClip(view.model, 160) : undefined,
|
|
4889
|
+
thinkingLevel: view.thinkingLevel ? displayClip(view.thinkingLevel, 80) : undefined,
|
|
4776
4890
|
locale: en ? "en" : "zh",
|
|
4777
4891
|
}, statusEmoji);
|
|
4778
4892
|
return {
|
|
4779
4893
|
schema: "2.0",
|
|
4780
4894
|
config: { update_multi: true, width_mode: "fill" },
|
|
4781
|
-
body: { elements: [{ tag: "markdown", content: `<font color='grey'>${summary}</font>` }] },
|
|
4895
|
+
body: { elements: [...inputElements, { tag: "markdown", content: `<font color='grey'>${summary}</font>` }] },
|
|
4782
4896
|
};
|
|
4783
4897
|
}
|
|
4784
|
-
const elements = [];
|
|
4898
|
+
const elements = [...inputElements];
|
|
4785
4899
|
// Running OR failed: show the recent activity window (each line prefixed with
|
|
4786
4900
|
// the relative time mm:ss). On failure we deliberately KEEP the last lines so
|
|
4787
4901
|
// the steps leading up to the error/kill/timeout are visible for debugging.
|
|
@@ -4794,24 +4908,26 @@ export class FeishuBot {
|
|
|
4794
4908
|
: "•";
|
|
4795
4909
|
const fmtAt = (sec) => `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, "0")}`;
|
|
4796
4910
|
const content = view.lines
|
|
4911
|
+
.filter(l => !l.inputHint && !(view.inputStatus?.botName && l.kind === 'lifecycle'))
|
|
4797
4912
|
.map((l) => {
|
|
4798
4913
|
const stamp = l.kind === "summary" ? "" : `\`${fmtAt(l.at)}\` `;
|
|
4799
|
-
return `${stamp}${iconFor(l.kind)} ${this.escapeCardText(l.text)}`;
|
|
4914
|
+
return `${stamp}${iconFor(l.kind)} ${this.escapeCardText(displayClip(l.text, limits.activity))}`;
|
|
4800
4915
|
})
|
|
4801
4916
|
.join("\n");
|
|
4802
|
-
|
|
4917
|
+
if (content)
|
|
4918
|
+
elements.push({ tag: "markdown", content });
|
|
4803
4919
|
}
|
|
4804
|
-
else {
|
|
4920
|
+
else if (!inputElements.length) {
|
|
4805
4921
|
elements.push({ tag: "markdown", content: view.state === "queued" ? (en ? "_Received locally; waiting for admission_" : "_本地已收件,等待提交确认_") : view.state === "admitted" ? (en ? "_Accepted; waiting for activity_" : "_已接收,等待活动_") : (en ? "_working…_" : "_正在启动…_") });
|
|
4806
4922
|
}
|
|
4807
4923
|
elements.push({ tag: "hr" });
|
|
4808
4924
|
// Keep the active model visible while the run is in progress or failed.
|
|
4809
4925
|
// The final reply also keeps model attribution so it remains self-contained.
|
|
4810
|
-
const footerBits = [en ? `⏱ ${view.elapsed}` : `⏱ 已用 ${view.elapsed}`];
|
|
4926
|
+
const footerBits = [en ? `⏱ ${displayClip(view.elapsed, 32)}` : `⏱ 已用 ${displayClip(view.elapsed, 32)}`];
|
|
4811
4927
|
if (view.model?.trim()) {
|
|
4812
4928
|
const modelBit = view.thinkingLevel?.trim()
|
|
4813
|
-
? `🧠 ${this.escapeCardText(view.model.trim())} · ${this.escapeCardText(view.thinkingLevel.trim())}`
|
|
4814
|
-
: `🧠 ${this.escapeCardText(view.model.trim())}`;
|
|
4929
|
+
? `🧠 ${this.escapeCardText(displayClip(view.model.trim(), 160))} · ${this.escapeCardText(displayClip(view.thinkingLevel.trim(), 80))}`
|
|
4930
|
+
: `🧠 ${this.escapeCardText(displayClip(view.model.trim(), 160))}`;
|
|
4815
4931
|
footerBits.push(modelBit);
|
|
4816
4932
|
}
|
|
4817
4933
|
elements.push({ tag: "markdown", content: `<font color='grey'>${footerBits.join(" · ")}</font>` });
|
|
@@ -4822,7 +4938,7 @@ export class FeishuBot {
|
|
|
4822
4938
|
// rejects patches on cards that did not declare it before and after.
|
|
4823
4939
|
config: { update_multi: true, width_mode: "fill" },
|
|
4824
4940
|
header: {
|
|
4825
|
-
title: { tag: "plain_text", content: view.title },
|
|
4941
|
+
title: { tag: "plain_text", content: displayClip(view.title, 160) },
|
|
4826
4942
|
template,
|
|
4827
4943
|
},
|
|
4828
4944
|
body: { elements },
|
|
@@ -4859,14 +4975,8 @@ export class FeishuBot {
|
|
|
4859
4975
|
}
|
|
4860
4976
|
async sendLiveStatusCard(chatId, view) {
|
|
4861
4977
|
try {
|
|
4862
|
-
const
|
|
4863
|
-
|
|
4864
|
-
data: {
|
|
4865
|
-
receive_id: chatId,
|
|
4866
|
-
content: JSON.stringify(this.buildLiveStatusCard(view, chatId)),
|
|
4867
|
-
msg_type: "interactive",
|
|
4868
|
-
},
|
|
4869
|
-
});
|
|
4978
|
+
const request = this.liveStatusRequest(view, chatId, card => ({ params: { receive_id_type: "chat_id" }, data: { receive_id: chatId, content: JSON.stringify(card), msg_type: "interactive" } }));
|
|
4979
|
+
const res = await this.client.im.message.create(request);
|
|
4870
4980
|
if (typeof res.code === "number" && res.code !== 0)
|
|
4871
4981
|
throw new LiveStatusEffectRejectedError(`Feishu rejected card: ${res.code}`);
|
|
4872
4982
|
if (res.code !== 0)
|
|
@@ -4881,13 +4991,8 @@ export class FeishuBot {
|
|
|
4881
4991
|
}
|
|
4882
4992
|
}
|
|
4883
4993
|
async replyLiveStatusCard(messageId, view, chatId) {
|
|
4884
|
-
const
|
|
4885
|
-
|
|
4886
|
-
data: {
|
|
4887
|
-
content: JSON.stringify(this.buildLiveStatusCard(view, chatId)),
|
|
4888
|
-
msg_type: "interactive",
|
|
4889
|
-
},
|
|
4890
|
-
});
|
|
4994
|
+
const request = this.liveStatusRequest(view, chatId, card => ({ path: { message_id: messageId }, data: { content: JSON.stringify(card), msg_type: "interactive" } }));
|
|
4995
|
+
const res = await this.client.im.message.reply(request);
|
|
4891
4996
|
if (typeof res.code === "number" && res.code !== 0)
|
|
4892
4997
|
throw new LiveStatusEffectRejectedError(`Feishu rejected card: ${res.code}`);
|
|
4893
4998
|
if (res.code !== 0)
|
|
@@ -4895,10 +5000,8 @@ export class FeishuBot {
|
|
|
4895
5000
|
return res?.data?.message_id || res?.message_id;
|
|
4896
5001
|
}
|
|
4897
5002
|
async patchLiveStatusCard(messageId, view, chatId) {
|
|
4898
|
-
const
|
|
4899
|
-
|
|
4900
|
-
data: { content: JSON.stringify(this.buildLiveStatusCard(view, chatId)) },
|
|
4901
|
-
});
|
|
5003
|
+
const request = this.liveStatusRequest(view, chatId, card => ({ path: { message_id: messageId }, data: { content: JSON.stringify(card) } }));
|
|
5004
|
+
const res = await this.client.im.message.patch(request);
|
|
4902
5005
|
if (typeof res.code === "number" && res.code !== 0)
|
|
4903
5006
|
throw new LiveStatusEffectRejectedError(`Feishu rejected card: ${res.code}`);
|
|
4904
5007
|
if (res.code !== 0)
|
|
@@ -6158,28 +6261,17 @@ export class FeishuBot {
|
|
|
6158
6261
|
* Called once per chat on first message.
|
|
6159
6262
|
*/
|
|
6160
6263
|
async fetchAndCacheChatInfo(chatId, chatType) {
|
|
6264
|
+
if (chatType === "p2p") {
|
|
6265
|
+
// Private ownership is not hourly metadata. Check/pin the stable SDK app
|
|
6266
|
+
// identity even on a fresh cache; never replace an unknown legacy owner.
|
|
6267
|
+
this.store.claimP2pOwner(chatId, this.config.name, this.config.appId);
|
|
6268
|
+
return;
|
|
6269
|
+
}
|
|
6161
6270
|
const existing = this.store.getChatInfo(chatId);
|
|
6162
6271
|
// Refresh at most once per hour
|
|
6163
6272
|
if (existing && Date.now() - existing.updatedAt < 3600_000)
|
|
6164
6273
|
return;
|
|
6165
6274
|
try {
|
|
6166
|
-
if (chatType === "p2p") {
|
|
6167
|
-
// For p2p, we don't have a group name or member list from chat API
|
|
6168
|
-
this.store.upsertChatInfo({
|
|
6169
|
-
chatId,
|
|
6170
|
-
chatType: "p2p",
|
|
6171
|
-
chatName: "私聊",
|
|
6172
|
-
members: "",
|
|
6173
|
-
memberNames: "",
|
|
6174
|
-
ownerBot: existing?.ownerBot || this.config.name,
|
|
6175
|
-
freeDiscussion: this.store.getChatInfo(chatId)?.freeDiscussion || false,
|
|
6176
|
-
verbose: this.store.getChatInfo(chatId)?.verbose || false,
|
|
6177
|
-
discuss: this.store.getChatInfo(chatId)?.discuss || false,
|
|
6178
|
-
discussMaxRounds: this.store.getChatInfo(chatId)?.discussMaxRounds || 10,
|
|
6179
|
-
updatedAt: Date.now(),
|
|
6180
|
-
});
|
|
6181
|
-
return;
|
|
6182
|
-
}
|
|
6183
6275
|
// Fetch group info
|
|
6184
6276
|
const chatResp = await this.client.im.chat.get({
|
|
6185
6277
|
path: { chat_id: chatId },
|
|
@@ -6428,6 +6520,28 @@ export class FeishuBot {
|
|
|
6428
6520
|
/**
|
|
6429
6521
|
* Extract text content from a rich text (post) message.
|
|
6430
6522
|
*/
|
|
6523
|
+
/** Visible rich-post fragments only. Text nodes remain literal; only actual
|
|
6524
|
+
* media nodes become type markers, never a regex over the user's prose. */
|
|
6525
|
+
visiblePostInput(content, chatId) {
|
|
6526
|
+
const parts = [];
|
|
6527
|
+
if (typeof content.title === 'string')
|
|
6528
|
+
parts.push(content.title);
|
|
6529
|
+
for (const paragraph of Array.isArray(content.content) ? content.content : []) {
|
|
6530
|
+
if (!Array.isArray(paragraph))
|
|
6531
|
+
continue;
|
|
6532
|
+
for (const node of paragraph) {
|
|
6533
|
+
if (node?.tag === 'text' && typeof node.text === 'string')
|
|
6534
|
+
parts.push(node.text);
|
|
6535
|
+
else if (node?.tag === 'a' && typeof node.text === 'string')
|
|
6536
|
+
parts.push(`${node.text}${typeof node.href === 'string' ? `(${node.href})` : ''}`);
|
|
6537
|
+
else if (node?.tag === 'at' && typeof node.user_name === 'string')
|
|
6538
|
+
parts.push(`@${node.user_name}`);
|
|
6539
|
+
else if (node?.tag === 'img')
|
|
6540
|
+
parts.push(this.isEn(chatId) ? '[Image]' : '[图片]');
|
|
6541
|
+
}
|
|
6542
|
+
}
|
|
6543
|
+
return parts.join(' ');
|
|
6544
|
+
}
|
|
6431
6545
|
extractPostText(content) {
|
|
6432
6546
|
const parts = [];
|
|
6433
6547
|
const title = content.title;
|