@xmoxmo/bncr 0.3.4 → 0.3.6
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/index.js +7 -3
- package/index.ts +6 -0
- package/openclaw.plugin.json +21 -0
- package/package.json +1 -1
- package/scripts/check-pack.mjs +97 -17
- package/scripts/check-register-drift.mjs +91 -65
- package/scripts/selfcheck.mjs +79 -3
- package/src/channel.ts +477 -635
- package/src/core/connection-capability.ts +2 -2
- package/src/core/connection-reachability.ts +106 -0
- package/src/core/dead-letter-diagnostics.ts +91 -0
- package/src/core/diagnostic-counters.ts +61 -0
- package/src/core/diagnostics.ts +9 -5
- package/src/core/downlink-health.ts +12 -7
- package/src/core/extended-diagnostics.ts +2 -0
- package/src/core/logging.ts +98 -0
- package/src/core/outbox-entry-builders.ts +13 -2
- package/src/core/persisted-outbox-entry.ts +53 -0
- package/src/core/probe.ts +33 -13
- package/src/core/register-trace.ts +48 -0
- package/src/core/status-meta.ts +77 -0
- package/src/core/status.ts +50 -57
- package/src/messaging/inbound/commands.ts +25 -86
- package/src/messaging/inbound/dispatch.ts +9 -36
- package/src/messaging/inbound/last-route.ts +46 -0
- package/src/messaging/inbound/native-command.ts +49 -0
- package/src/messaging/inbound/native-reply-delivery.ts +43 -0
- package/src/messaging/outbound/diagnostics.ts +221 -2
- package/src/messaging/outbound/reply-enqueue.ts +56 -1
- package/src/messaging/outbound/reply-target-policy.ts +4 -1
- package/src/messaging/outbound/send-params.ts +56 -0
- package/src/openclaw/runtime-surface.ts +29 -0
- package/src/plugin/gateway-methods.ts +2 -0
- package/src/plugin/status.ts +10 -4
- package/src/runtime/outbound-ack-timeout.ts +73 -0
- package/src/runtime/register-trace-runtime.ts +102 -0
- package/src/runtime/status-snapshots.ts +7 -3
- package/src/runtime/status-worker.ts +70 -11
|
@@ -1,7 +1,226 @@
|
|
|
1
|
-
import type { BncrConnection } from '../../core/types.ts';
|
|
1
|
+
import type { BncrConnection, OutboxEntry } from '../../core/types.ts';
|
|
2
2
|
import { OUTBOUND_TERMINAL_REASON, type OutboundScheduleSource } from './reasons.ts';
|
|
3
3
|
import type { RetryRerouteDecision } from './retry-policy.ts';
|
|
4
4
|
|
|
5
|
+
type OutboxIncidentSummaryInput = {
|
|
6
|
+
pending: number;
|
|
7
|
+
oldestPendingAt?: number | null;
|
|
8
|
+
lastAttemptAt?: number | null;
|
|
9
|
+
lastPushAt?: number | null;
|
|
10
|
+
lastPushError?: string | null;
|
|
11
|
+
hasGatewayContext: boolean;
|
|
12
|
+
activeOutboundConnection: boolean;
|
|
13
|
+
activeOutboundConnectionCount: number;
|
|
14
|
+
prePushGuardSkipCount?: number;
|
|
15
|
+
lastPrePushGuardSkipAt?: number | null;
|
|
16
|
+
lastPrePushGuardSkipReason?: string | null;
|
|
17
|
+
lastAckQueueLatencyMs?: number | null;
|
|
18
|
+
lastAckPushLatencyMs?: number | null;
|
|
19
|
+
lastLateAckQueueLatencyMs?: number | null;
|
|
20
|
+
lastLateAckPushLatencyMs?: number | null;
|
|
21
|
+
lastLateAckOkAt?: number | null;
|
|
22
|
+
adaptiveAckTimeoutMs?: number | null;
|
|
23
|
+
adaptiveAckTimeoutReason?: string | null;
|
|
24
|
+
nowMs?: number;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
function finiteNumberOrNull(value: unknown): number | null {
|
|
28
|
+
if (value == null) return null;
|
|
29
|
+
const n = Number(value);
|
|
30
|
+
return Number.isFinite(n) ? n : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function positiveAgeMs(nowMs: number, at: unknown): number | null {
|
|
34
|
+
const n = finiteNumberOrNull(at);
|
|
35
|
+
if (n === null || n < 0) return null;
|
|
36
|
+
return Math.max(0, nowMs - n);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function buildOutboxIncidentSummary(input: OutboxIncidentSummaryInput) {
|
|
40
|
+
const pending = Math.max(0, Math.floor(finiteNumberOrNull(input.pending) || 0));
|
|
41
|
+
const nowMs = finiteNumberOrNull(input.nowMs) || Date.now();
|
|
42
|
+
const lastPushError =
|
|
43
|
+
typeof input.lastPushError === 'string' && input.lastPushError ? input.lastPushError : null;
|
|
44
|
+
const lastPrePushGuardSkipReason =
|
|
45
|
+
typeof input.lastPrePushGuardSkipReason === 'string' && input.lastPrePushGuardSkipReason
|
|
46
|
+
? input.lastPrePushGuardSkipReason
|
|
47
|
+
: null;
|
|
48
|
+
const oldestPendingAgeMs = positiveAgeMs(nowMs, input.oldestPendingAt);
|
|
49
|
+
const lastLateAckAgeMs = positiveAgeMs(nowMs, input.lastLateAckOkAt);
|
|
50
|
+
const adaptiveAckTimeoutReason =
|
|
51
|
+
typeof input.adaptiveAckTimeoutReason === 'string' && input.adaptiveAckTimeoutReason
|
|
52
|
+
? input.adaptiveAckTimeoutReason
|
|
53
|
+
: null;
|
|
54
|
+
const hasRecentLateAck = lastLateAckAgeMs !== null && lastLateAckAgeMs <= 3_600_000;
|
|
55
|
+
const hasActiveAdaptiveAck =
|
|
56
|
+
adaptiveAckTimeoutReason !== null &&
|
|
57
|
+
![
|
|
58
|
+
'no-timeout-evidence',
|
|
59
|
+
'no-late-ack-evidence',
|
|
60
|
+
'missing-latency',
|
|
61
|
+
'late-ack-expired',
|
|
62
|
+
'recovered',
|
|
63
|
+
].includes(adaptiveAckTimeoutReason);
|
|
64
|
+
|
|
65
|
+
let type = 'none';
|
|
66
|
+
let severity: 'ok' | 'warning' | 'critical' = 'ok';
|
|
67
|
+
let recommendedAction = 'none';
|
|
68
|
+
|
|
69
|
+
if (pending > 0 && !input.hasGatewayContext) {
|
|
70
|
+
type = 'no-gateway-context';
|
|
71
|
+
severity = 'critical';
|
|
72
|
+
recommendedAction = 'check-channel-message-runtime-context';
|
|
73
|
+
} else if (pending > 0 && !input.activeOutboundConnection) {
|
|
74
|
+
type = 'no-active-outbound-connection';
|
|
75
|
+
severity = 'critical';
|
|
76
|
+
recommendedAction = 'reconnect-bncr-client';
|
|
77
|
+
} else if (pending > 0 && lastPrePushGuardSkipReason) {
|
|
78
|
+
type = lastPrePushGuardSkipReason;
|
|
79
|
+
severity = 'warning';
|
|
80
|
+
recommendedAction = 'inspect-pre-push-guard';
|
|
81
|
+
} else if (pending > 0 && lastPushError) {
|
|
82
|
+
type =
|
|
83
|
+
lastPushError.includes('ack') || lastPushError.includes('timeout')
|
|
84
|
+
? 'ack-timeout'
|
|
85
|
+
: lastPushError;
|
|
86
|
+
severity = type === 'ack-timeout' ? 'critical' : 'warning';
|
|
87
|
+
recommendedAction = 'inspect-ack-and-route-state';
|
|
88
|
+
} else if (pending > 0 && oldestPendingAgeMs !== null && oldestPendingAgeMs >= 120_000) {
|
|
89
|
+
type = 'outbox-backlog';
|
|
90
|
+
severity = 'warning';
|
|
91
|
+
recommendedAction = 'inspect-outbox-drain';
|
|
92
|
+
} else if (pending > 0) {
|
|
93
|
+
type = 'pending-outbox';
|
|
94
|
+
severity = 'warning';
|
|
95
|
+
recommendedAction = 'inspect-outbox-drain';
|
|
96
|
+
} else if (hasRecentLateAck || hasActiveAdaptiveAck) {
|
|
97
|
+
type = 'slow-or-late-ack';
|
|
98
|
+
severity = 'warning';
|
|
99
|
+
recommendedAction = 'inspect-ack-latency';
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
active: type !== 'none',
|
|
104
|
+
type,
|
|
105
|
+
severity,
|
|
106
|
+
recommendedAction,
|
|
107
|
+
pending,
|
|
108
|
+
oldestPendingAgeMs,
|
|
109
|
+
lastAttemptAgeMs: positiveAgeMs(nowMs, input.lastAttemptAt),
|
|
110
|
+
lastPushAgeMs: positiveAgeMs(nowMs, input.lastPushAt),
|
|
111
|
+
lastPushError,
|
|
112
|
+
hasGatewayContext: input.hasGatewayContext,
|
|
113
|
+
activeOutboundConnection: input.activeOutboundConnection,
|
|
114
|
+
activeOutboundConnectionCount: Math.max(
|
|
115
|
+
0,
|
|
116
|
+
Math.floor(finiteNumberOrNull(input.activeOutboundConnectionCount) || 0),
|
|
117
|
+
),
|
|
118
|
+
prePushGuardSkipCount: Math.max(
|
|
119
|
+
0,
|
|
120
|
+
Math.floor(finiteNumberOrNull(input.prePushGuardSkipCount) || 0),
|
|
121
|
+
),
|
|
122
|
+
lastPrePushGuardSkipAgeMs: positiveAgeMs(nowMs, input.lastPrePushGuardSkipAt),
|
|
123
|
+
lastPrePushGuardSkipReason,
|
|
124
|
+
ack: {
|
|
125
|
+
lastQueueLatencyMs: finiteNumberOrNull(input.lastAckQueueLatencyMs),
|
|
126
|
+
lastPushLatencyMs: finiteNumberOrNull(input.lastAckPushLatencyMs),
|
|
127
|
+
lastLateQueueLatencyMs: finiteNumberOrNull(input.lastLateAckQueueLatencyMs),
|
|
128
|
+
lastLatePushLatencyMs: finiteNumberOrNull(input.lastLateAckPushLatencyMs),
|
|
129
|
+
lastLateAckAgeMs,
|
|
130
|
+
adaptiveTimeoutMs: finiteNumberOrNull(input.adaptiveAckTimeoutMs),
|
|
131
|
+
adaptiveTimeoutReason: adaptiveAckTimeoutReason,
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function buildExtendedOutboundDiagnostics(input: {
|
|
137
|
+
outbox: Record<string, any>;
|
|
138
|
+
enqueueCount: number;
|
|
139
|
+
lastEnqueueAt?: number | null;
|
|
140
|
+
prePushGuardSkipCount: number;
|
|
141
|
+
lastPrePushGuardSkipAt?: number | null;
|
|
142
|
+
lastPrePushGuardSkipReason?: string | null;
|
|
143
|
+
hasGatewayContext: boolean;
|
|
144
|
+
lastGatewayContextAt?: number | null;
|
|
145
|
+
ackObservability: Record<string, any>;
|
|
146
|
+
nowMs?: number;
|
|
147
|
+
}) {
|
|
148
|
+
const lastEnqueueAt = finiteNumberOrNull(input.lastEnqueueAt);
|
|
149
|
+
const lastPrePushGuardSkipAt = finiteNumberOrNull(input.lastPrePushGuardSkipAt);
|
|
150
|
+
const lastGatewayContextAt = finiteNumberOrNull(input.lastGatewayContextAt);
|
|
151
|
+
return {
|
|
152
|
+
...input.outbox,
|
|
153
|
+
enqueueCount: input.enqueueCount,
|
|
154
|
+
lastEnqueueAt,
|
|
155
|
+
prePushGuardSkipCount: input.prePushGuardSkipCount,
|
|
156
|
+
lastPrePushGuardSkipAt,
|
|
157
|
+
lastPrePushGuardSkipReason: input.lastPrePushGuardSkipReason || null,
|
|
158
|
+
hasGatewayContext: input.hasGatewayContext,
|
|
159
|
+
lastGatewayContextAt,
|
|
160
|
+
incident: buildOutboxIncidentSummary({
|
|
161
|
+
...input.outbox,
|
|
162
|
+
hasGatewayContext: input.hasGatewayContext,
|
|
163
|
+
prePushGuardSkipCount: input.prePushGuardSkipCount,
|
|
164
|
+
lastPrePushGuardSkipAt,
|
|
165
|
+
lastPrePushGuardSkipReason: input.lastPrePushGuardSkipReason || null,
|
|
166
|
+
lastAckQueueLatencyMs: input.ackObservability.lastAckQueueLatencyMs,
|
|
167
|
+
lastAckPushLatencyMs: input.ackObservability.lastAckPushLatencyMs,
|
|
168
|
+
lastLateAckQueueLatencyMs: input.ackObservability.lastLateAckQueueLatencyMs,
|
|
169
|
+
lastLateAckPushLatencyMs: input.ackObservability.lastLateAckPushLatencyMs,
|
|
170
|
+
lastLateAckOkAt: input.ackObservability.lastLateAckOkAt,
|
|
171
|
+
adaptiveAckTimeoutMs: input.ackObservability.currentAckTimeoutMs,
|
|
172
|
+
adaptiveAckTimeoutReason: input.ackObservability.recommendedAckTimeoutReason,
|
|
173
|
+
nowMs: input.nowMs,
|
|
174
|
+
}),
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function buildOutboxQueueDiagnostics(args: {
|
|
179
|
+
accountId: string;
|
|
180
|
+
outboxEntries: Iterable<OutboxEntry>;
|
|
181
|
+
pendingAllAccounts: number;
|
|
182
|
+
pushConnIds: Iterable<string>;
|
|
183
|
+
}) {
|
|
184
|
+
const accountEntries = Array.from(args.outboxEntries).filter(
|
|
185
|
+
(entry) => entry.accountId === args.accountId,
|
|
186
|
+
);
|
|
187
|
+
let oldestPendingAt: number | null = null;
|
|
188
|
+
let newestPendingAt: number | null = null;
|
|
189
|
+
let lastAttemptAt: number | null = null;
|
|
190
|
+
let lastPushAt: number | null = null;
|
|
191
|
+
let lastError: string | null = null;
|
|
192
|
+
|
|
193
|
+
for (const entry of accountEntries) {
|
|
194
|
+
const createdAt = Number(entry.createdAt);
|
|
195
|
+
if (Number.isFinite(createdAt)) {
|
|
196
|
+
oldestPendingAt = oldestPendingAt === null ? createdAt : Math.min(oldestPendingAt, createdAt);
|
|
197
|
+
newestPendingAt = newestPendingAt === null ? createdAt : Math.max(newestPendingAt, createdAt);
|
|
198
|
+
}
|
|
199
|
+
const attemptAt = Number(entry.lastAttemptAt);
|
|
200
|
+
if (Number.isFinite(attemptAt) && (lastAttemptAt === null || attemptAt > lastAttemptAt)) {
|
|
201
|
+
lastAttemptAt = attemptAt;
|
|
202
|
+
}
|
|
203
|
+
const pushAt = Number(entry.lastPushAt);
|
|
204
|
+
if (Number.isFinite(pushAt) && (lastPushAt === null || pushAt > lastPushAt)) {
|
|
205
|
+
lastPushAt = pushAt;
|
|
206
|
+
lastError = entry.lastError || null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const pushConnIds = Array.from(args.pushConnIds);
|
|
211
|
+
return {
|
|
212
|
+
pending: accountEntries.length,
|
|
213
|
+
pendingAllAccounts: args.pendingAllAccounts,
|
|
214
|
+
oldestPendingAt,
|
|
215
|
+
newestPendingAt,
|
|
216
|
+
lastAttemptAt,
|
|
217
|
+
lastPushAt,
|
|
218
|
+
lastPushError: lastError,
|
|
219
|
+
activeOutboundConnection: pushConnIds.length > 0,
|
|
220
|
+
activeOutboundConnectionCount: pushConnIds.length,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
5
224
|
export function buildOutboxScheduleDebugInfo(args: {
|
|
6
225
|
bridgeId: string;
|
|
7
226
|
accountId?: string | null;
|
|
@@ -187,7 +406,7 @@ export function buildOutboxDrainStuckDebugInfo(args: {
|
|
|
187
406
|
outboxSize: args.outboxSize,
|
|
188
407
|
pending: args.pending,
|
|
189
408
|
runningMs: args.runningMs,
|
|
190
|
-
runningSince: args.runningSince
|
|
409
|
+
runningSince: args.runningSince ?? null,
|
|
191
410
|
hasGatewayContext: args.hasGatewayContext,
|
|
192
411
|
activeConnectionCount: args.activeConnectionCount,
|
|
193
412
|
waiters: {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
|
|
2
2
|
import { buildReplyMediaFallbackDebugInfo } from './diagnostics.ts';
|
|
3
|
+
import type { OutboundReplyTargetPolicy } from './reply-target-policy.ts';
|
|
3
4
|
import { normalizeOutboundReplyToId } from './reply-target-policy.ts';
|
|
4
5
|
|
|
6
|
+
export type { OutboundReplyTargetPolicy } from './reply-target-policy.ts';
|
|
7
|
+
|
|
5
8
|
export type ReplyPayloadInput = {
|
|
6
9
|
text?: string;
|
|
7
10
|
mediaUrl?: string;
|
|
@@ -21,6 +24,7 @@ export type NormalizedReplyPayload = {
|
|
|
21
24
|
audioAsVoice: boolean;
|
|
22
25
|
kind?: 'tool' | 'block' | 'final';
|
|
23
26
|
replyToId: string;
|
|
27
|
+
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
24
28
|
};
|
|
25
29
|
|
|
26
30
|
export type ReplyMediaEntriesParams = {
|
|
@@ -51,6 +55,7 @@ export type ReplyMediaFileTransferParams = {
|
|
|
51
55
|
audioAsVoice: boolean;
|
|
52
56
|
kind?: 'tool' | 'block' | 'final';
|
|
53
57
|
replyToId: string;
|
|
58
|
+
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
54
59
|
createdAt: number;
|
|
55
60
|
};
|
|
56
61
|
|
|
@@ -70,13 +75,29 @@ export type ReplyMediaFallbackTextEntryParams = {
|
|
|
70
75
|
mediaUrl: string;
|
|
71
76
|
kind?: 'tool' | 'block' | 'final';
|
|
72
77
|
replyToId: string;
|
|
78
|
+
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
73
79
|
fallback: { text: string; reason: string };
|
|
74
80
|
};
|
|
75
81
|
|
|
82
|
+
const MEDIA_TEXT_SPLIT_THRESHOLD = 1020;
|
|
83
|
+
|
|
76
84
|
export function hasReplyMediaEntries(payload: NormalizedReplyPayload) {
|
|
77
85
|
return payload.mediaList.length > 0;
|
|
78
86
|
}
|
|
79
87
|
|
|
88
|
+
export function shouldSplitReplyMediaText(payload: NormalizedReplyPayload) {
|
|
89
|
+
if (!payload.text) return false;
|
|
90
|
+
if (payload.mediaList.length > 1) return true;
|
|
91
|
+
return payload.text.length > MEDIA_TEXT_SPLIT_THRESHOLD;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function withoutReplyMediaText(payload: NormalizedReplyPayload): NormalizedReplyPayload {
|
|
95
|
+
return {
|
|
96
|
+
...payload,
|
|
97
|
+
text: '',
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
80
101
|
export function buildReplyTextOutboxEntry(
|
|
81
102
|
params: {
|
|
82
103
|
accountId: string;
|
|
@@ -85,6 +106,7 @@ export function buildReplyTextOutboxEntry(
|
|
|
85
106
|
text: string;
|
|
86
107
|
kind?: 'tool' | 'block' | 'final';
|
|
87
108
|
replyToId: string;
|
|
109
|
+
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
88
110
|
},
|
|
89
111
|
helpers: {
|
|
90
112
|
buildTextOutboxEntry: (args: {
|
|
@@ -94,6 +116,7 @@ export function buildReplyTextOutboxEntry(
|
|
|
94
116
|
text: string;
|
|
95
117
|
kind?: 'tool' | 'block' | 'final';
|
|
96
118
|
replyToId?: string;
|
|
119
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
97
120
|
}) => OutboxEntry;
|
|
98
121
|
},
|
|
99
122
|
): OutboxEntry {
|
|
@@ -104,6 +127,7 @@ export function buildReplyTextOutboxEntry(
|
|
|
104
127
|
text: params.text,
|
|
105
128
|
kind: params.kind,
|
|
106
129
|
replyToId: params.replyToId || undefined,
|
|
130
|
+
replyTargetPolicy: params.replyTargetPolicy,
|
|
107
131
|
});
|
|
108
132
|
}
|
|
109
133
|
|
|
@@ -123,6 +147,7 @@ export function enqueueReplyTextEntry(
|
|
|
123
147
|
text: string;
|
|
124
148
|
kind?: 'tool' | 'block' | 'final';
|
|
125
149
|
replyToId?: string;
|
|
150
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
126
151
|
}) => OutboxEntry;
|
|
127
152
|
},
|
|
128
153
|
): void {
|
|
@@ -137,6 +162,7 @@ export function enqueueReplyTextEntry(
|
|
|
137
162
|
text: params.payload.text,
|
|
138
163
|
kind: params.payload.kind,
|
|
139
164
|
replyToId: params.payload.replyToId,
|
|
165
|
+
replyTargetPolicy: params.payload.replyTargetPolicy,
|
|
140
166
|
},
|
|
141
167
|
{ buildTextOutboxEntry: helpers.buildTextOutboxEntry },
|
|
142
168
|
),
|
|
@@ -159,6 +185,7 @@ export function enqueueReplyMediaFallbackTextEntry(
|
|
|
159
185
|
text: string;
|
|
160
186
|
kind?: 'tool' | 'block' | 'final';
|
|
161
187
|
replyToId?: string;
|
|
188
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
162
189
|
}) => OutboxEntry;
|
|
163
190
|
},
|
|
164
191
|
): void {
|
|
@@ -176,6 +203,7 @@ export function enqueueReplyMediaFallbackTextEntry(
|
|
|
176
203
|
text: params.fallback.text,
|
|
177
204
|
kind: params.kind,
|
|
178
205
|
replyToId: params.replyToId,
|
|
206
|
+
replyTargetPolicy: params.replyTargetPolicy,
|
|
179
207
|
},
|
|
180
208
|
{ buildTextOutboxEntry: helpers.buildTextOutboxEntry },
|
|
181
209
|
),
|
|
@@ -197,6 +225,7 @@ export function enqueueReplyMediaFileTransferEntry(
|
|
|
197
225
|
audioAsVoice: boolean;
|
|
198
226
|
kind?: 'tool' | 'block' | 'final';
|
|
199
227
|
replyToId?: string;
|
|
228
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
200
229
|
}) => OutboxEntry;
|
|
201
230
|
rememberRecentMediaSend: (args: {
|
|
202
231
|
sessionKey: string;
|
|
@@ -219,6 +248,7 @@ export function enqueueReplyMediaFileTransferEntry(
|
|
|
219
248
|
audioAsVoice: params.audioAsVoice,
|
|
220
249
|
kind: params.kind,
|
|
221
250
|
replyToId: params.replyToId || undefined,
|
|
251
|
+
replyTargetPolicy: params.replyTargetPolicy,
|
|
222
252
|
}),
|
|
223
253
|
);
|
|
224
254
|
helpers.rememberRecentMediaSend({
|
|
@@ -245,6 +275,7 @@ export function enqueueSingleReplyMediaEntry(
|
|
|
245
275
|
mediaUrl: params.mediaUrl,
|
|
246
276
|
kind: params.params.payload.kind,
|
|
247
277
|
replyToId: params.params.payload.replyToId,
|
|
278
|
+
replyTargetPolicy: params.params.payload.replyTargetPolicy,
|
|
248
279
|
fallback: params.fallback,
|
|
249
280
|
});
|
|
250
281
|
return;
|
|
@@ -262,6 +293,7 @@ export function enqueueSingleReplyMediaEntry(
|
|
|
262
293
|
audioAsVoice: params.params.payload.audioAsVoice,
|
|
263
294
|
kind: params.params.payload.kind,
|
|
264
295
|
replyToId: params.params.payload.replyToId,
|
|
296
|
+
replyTargetPolicy: params.params.payload.replyTargetPolicy,
|
|
265
297
|
createdAt: params.currentTime,
|
|
266
298
|
});
|
|
267
299
|
}
|
|
@@ -293,6 +325,23 @@ export function enqueueNormalizedReplyPayload(
|
|
|
293
325
|
});
|
|
294
326
|
|
|
295
327
|
if (helpers.hasReplyMediaEntries(params.payload)) {
|
|
328
|
+
if (shouldSplitReplyMediaText(params.payload)) {
|
|
329
|
+
helpers.enqueueReplyTextEntry({
|
|
330
|
+
accountId: params.accountId,
|
|
331
|
+
sessionKey: params.sessionKey,
|
|
332
|
+
route: params.route,
|
|
333
|
+
payload: params.payload,
|
|
334
|
+
});
|
|
335
|
+
helpers.enqueueReplyMediaEntries({
|
|
336
|
+
accountId: params.accountId,
|
|
337
|
+
sessionKey: params.sessionKey,
|
|
338
|
+
route: params.route,
|
|
339
|
+
payload: withoutReplyMediaText(params.payload),
|
|
340
|
+
mediaLocalRoots: params.mediaLocalRoots,
|
|
341
|
+
});
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
296
345
|
helpers.enqueueReplyMediaEntries({
|
|
297
346
|
accountId: params.accountId,
|
|
298
347
|
sessionKey: params.sessionKey,
|
|
@@ -314,6 +363,7 @@ export function enqueueNormalizedReplyPayload(
|
|
|
314
363
|
export function normalizeReplyPayload(
|
|
315
364
|
payload: ReplyPayloadInput,
|
|
316
365
|
helpers: { asString: (value: unknown, fallback?: string) => string },
|
|
366
|
+
options?: { replyTargetPolicy?: OutboundReplyTargetPolicy },
|
|
317
367
|
): NormalizedReplyPayload {
|
|
318
368
|
const text = helpers.asString(payload?.text || '').trim();
|
|
319
369
|
const mediaUrl = helpers.asString(payload?.mediaUrl || '').trim();
|
|
@@ -328,6 +378,11 @@ export function normalizeReplyPayload(
|
|
|
328
378
|
asVoice: payload?.asVoice === true,
|
|
329
379
|
audioAsVoice: payload?.audioAsVoice === true,
|
|
330
380
|
kind: payload?.kind,
|
|
331
|
-
|
|
381
|
+
replyTargetPolicy: options?.replyTargetPolicy ?? 'agent-default',
|
|
382
|
+
replyToId: normalizeOutboundReplyToId({
|
|
383
|
+
kind: payload?.kind,
|
|
384
|
+
replyToId: payload?.replyToId,
|
|
385
|
+
replyTargetPolicy: options?.replyTargetPolicy,
|
|
386
|
+
}),
|
|
332
387
|
};
|
|
333
388
|
}
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
import { normalizeReplyToId } from './media-dedupe.ts';
|
|
2
2
|
|
|
3
3
|
export type OutboundReplyKind = 'tool' | 'block' | 'final';
|
|
4
|
+
export type OutboundReplyTargetPolicy = 'agent-default' | 'preserve';
|
|
4
5
|
|
|
5
6
|
const STRIP_TOOL_REPLY_TO_ID = true;
|
|
6
7
|
|
|
7
8
|
export function normalizeOutboundReplyToId(params: {
|
|
8
9
|
kind?: OutboundReplyKind;
|
|
9
10
|
replyToId?: string | null;
|
|
11
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
10
12
|
}) {
|
|
11
|
-
if (params.kind === 'tool' && STRIP_TOOL_REPLY_TO_ID
|
|
13
|
+
if (params.kind === 'tool' && STRIP_TOOL_REPLY_TO_ID && params.replyTargetPolicy !== 'preserve')
|
|
14
|
+
return '';
|
|
12
15
|
return normalizeReplyToId(params.replyToId);
|
|
13
16
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { normalizeAccountId } from '../../core/accounts.ts';
|
|
2
|
+
import { readOpenClawBooleanParam, readOpenClawStringParam } from '../../openclaw/sdk-helpers.ts';
|
|
3
|
+
|
|
4
|
+
export type NormalizedBncrSendParams = {
|
|
5
|
+
to: string;
|
|
6
|
+
accountId: string;
|
|
7
|
+
message: string;
|
|
8
|
+
caption: string;
|
|
9
|
+
mediaUrl?: string;
|
|
10
|
+
asVoice: boolean;
|
|
11
|
+
audioAsVoice: boolean;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
15
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function normalizeBncrSendParams(input: {
|
|
19
|
+
params: unknown;
|
|
20
|
+
accountId: string;
|
|
21
|
+
}): NormalizedBncrSendParams {
|
|
22
|
+
const paramsObj = isPlainObject(input.params) ? input.params : {};
|
|
23
|
+
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
24
|
+
const resolvedAccountId = normalizeAccountId(
|
|
25
|
+
readOpenClawStringParam(paramsObj, 'accountId') ?? input.accountId,
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
const message = readOpenClawStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
|
|
29
|
+
const caption = readOpenClawStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
|
|
30
|
+
const mediaUrl =
|
|
31
|
+
readOpenClawStringParam(paramsObj, 'media', { trim: false }) ??
|
|
32
|
+
readOpenClawStringParam(paramsObj, 'path', { trim: false }) ??
|
|
33
|
+
readOpenClawStringParam(paramsObj, 'filePath', { trim: false }) ??
|
|
34
|
+
readOpenClawStringParam(paramsObj, 'mediaUrl', { trim: false });
|
|
35
|
+
const asVoice = readOpenClawBooleanParam(paramsObj, 'asVoice') ?? false;
|
|
36
|
+
const audioAsVoice = readOpenClawBooleanParam(paramsObj, 'audioAsVoice') ?? false;
|
|
37
|
+
|
|
38
|
+
if (asVoice && !mediaUrl) throw new Error('send voice requires media path');
|
|
39
|
+
|
|
40
|
+
const normalizedMessage = mediaUrl ? '' : message || caption || '';
|
|
41
|
+
const normalizedCaption = mediaUrl ? caption || message || '' : '';
|
|
42
|
+
|
|
43
|
+
if (!normalizedMessage.trim() && !normalizedCaption.trim() && !mediaUrl) {
|
|
44
|
+
throw new Error('send requires message or media');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
to,
|
|
49
|
+
accountId: resolvedAccountId,
|
|
50
|
+
message: normalizedMessage,
|
|
51
|
+
caption: normalizedCaption,
|
|
52
|
+
mediaUrl: mediaUrl || undefined,
|
|
53
|
+
asVoice,
|
|
54
|
+
audioAsVoice,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export type OpenClawChannelRuntimeSurfaceDiagnostics = {
|
|
2
|
+
channel: {
|
|
3
|
+
inbound: boolean;
|
|
4
|
+
media: boolean;
|
|
5
|
+
reply: boolean;
|
|
6
|
+
routing: boolean;
|
|
7
|
+
session: boolean;
|
|
8
|
+
};
|
|
9
|
+
missing: string[];
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function buildOpenClawChannelRuntimeSurfaceDiagnostics(
|
|
13
|
+
api: unknown,
|
|
14
|
+
): OpenClawChannelRuntimeSurfaceDiagnostics {
|
|
15
|
+
const channelRuntime = (api as any)?.runtime?.channel;
|
|
16
|
+
const surfaces = {
|
|
17
|
+
inbound: Boolean(channelRuntime?.inbound),
|
|
18
|
+
media: Boolean(channelRuntime?.media),
|
|
19
|
+
reply: Boolean(channelRuntime?.reply),
|
|
20
|
+
routing: Boolean(channelRuntime?.routing),
|
|
21
|
+
session: Boolean(channelRuntime?.session),
|
|
22
|
+
};
|
|
23
|
+
return {
|
|
24
|
+
channel: surfaces,
|
|
25
|
+
missing: Object.entries(surfaces)
|
|
26
|
+
.filter(([, present]) => !present)
|
|
27
|
+
.map(([name]) => name),
|
|
28
|
+
};
|
|
29
|
+
}
|
package/src/plugin/status.ts
CHANGED
|
@@ -22,13 +22,19 @@ export function createBncrStatusSurface(getBridge: () => BncrStatusBridge) {
|
|
|
22
22
|
},
|
|
23
23
|
buildAccountSnapshot: async ({ account, runtime }: any) => {
|
|
24
24
|
const runtimeBridge = getBridge();
|
|
25
|
-
const
|
|
25
|
+
const accountId = account?.accountId || BNCR_DEFAULT_ACCOUNT_ID;
|
|
26
|
+
const snapshotAccount = {
|
|
27
|
+
accountId,
|
|
28
|
+
name: account?.name,
|
|
29
|
+
enabled: account?.enabled,
|
|
30
|
+
};
|
|
31
|
+
const rt = runtime || runtimeBridge.getAccountRuntimeSnapshot(accountId);
|
|
26
32
|
return buildAccountStatusSnapshot({
|
|
27
|
-
account,
|
|
33
|
+
account: snapshotAccount,
|
|
28
34
|
runtime: rt,
|
|
29
|
-
healthSummary: runtimeBridge.getStatusHeadline(
|
|
35
|
+
healthSummary: runtimeBridge.getStatusHeadline(accountId),
|
|
30
36
|
// default 名不可隐藏时,统一展示稳定默认值
|
|
31
|
-
displayName: resolveDefaultDisplayName(account?.name,
|
|
37
|
+
displayName: resolveDefaultDisplayName(account?.name, accountId),
|
|
32
38
|
});
|
|
33
39
|
},
|
|
34
40
|
resolveAccountState: ({ enabled, configured, account, cfg, runtime }: any) => {
|
|
@@ -65,6 +65,15 @@ export function computeBncrRecommendedAckTimeoutMs(args: ComputeBncrRecommendedA
|
|
|
65
65
|
return Math.min(args.maxAckTimeoutMs, Math.max(args.minAckTimeoutMs, recommended));
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
export function resolveBncrRuntimeAckTimeoutDecision(args: ComputeBncrRecommendedAckTimeoutArgs) {
|
|
69
|
+
const timeoutMs = computeBncrRecommendedAckTimeoutMs(args);
|
|
70
|
+
const reason = computeBncrRecommendedAckTimeoutReason({
|
|
71
|
+
...args,
|
|
72
|
+
recommendedAckTimeoutMs: timeoutMs,
|
|
73
|
+
});
|
|
74
|
+
return { timeoutMs, reason };
|
|
75
|
+
}
|
|
76
|
+
|
|
68
77
|
function finiteNumberOr(value: unknown, fallback: number) {
|
|
69
78
|
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
70
79
|
}
|
|
@@ -94,3 +103,67 @@ export function buildBncrRuntimeAckStrategy(args: {
|
|
|
94
103
|
recovered: ackObservability.adaptiveAckRecovered === true,
|
|
95
104
|
};
|
|
96
105
|
}
|
|
106
|
+
|
|
107
|
+
export function buildBncrRuntimeAckObservability(args: {
|
|
108
|
+
lastAckOkAt: number | null;
|
|
109
|
+
lastAckTimeoutAt: number | null;
|
|
110
|
+
recentAckTimeoutCount: number;
|
|
111
|
+
lateAckOkCount: number;
|
|
112
|
+
lastLateAckOkAt: number | null;
|
|
113
|
+
adaptiveAckRecoveryOkCount: number;
|
|
114
|
+
lastAckQueueLatencyMs: number | null;
|
|
115
|
+
lastAckPushLatencyMs: number | null;
|
|
116
|
+
lastLateAckQueueLatencyMs: number | null;
|
|
117
|
+
lastLateAckPushLatencyMs: number | null;
|
|
118
|
+
adaptiveAckTimeoutEnabled: boolean;
|
|
119
|
+
defaultAckTimeoutMs: number;
|
|
120
|
+
currentAckTimeoutMs: number;
|
|
121
|
+
minAckTimeoutMs: number;
|
|
122
|
+
maxAckTimeoutMs: number;
|
|
123
|
+
lateAckObservationTtlMs: number;
|
|
124
|
+
recoveryOkThreshold: number;
|
|
125
|
+
nowMs: number;
|
|
126
|
+
}) {
|
|
127
|
+
const lastLateAckAgeMs =
|
|
128
|
+
typeof args.lastLateAckOkAt === 'number' && args.lastLateAckOkAt > 0
|
|
129
|
+
? Math.max(0, args.nowMs - args.lastLateAckOkAt)
|
|
130
|
+
: null;
|
|
131
|
+
const lateAckObservationExpired =
|
|
132
|
+
typeof lastLateAckAgeMs === 'number' && lastLateAckAgeMs > args.lateAckObservationTtlMs;
|
|
133
|
+
const adaptiveAckRecovered = args.adaptiveAckRecoveryOkCount >= args.recoveryOkThreshold;
|
|
134
|
+
const ackTimeoutDecision = resolveBncrRuntimeAckTimeoutDecision({
|
|
135
|
+
lateAckOkCount: args.lateAckOkCount,
|
|
136
|
+
recentAckTimeoutCount: args.recentAckTimeoutCount,
|
|
137
|
+
lastLateAckPushLatencyMs: args.lastLateAckPushLatencyMs,
|
|
138
|
+
lastLateAckOkAt: args.lastLateAckOkAt,
|
|
139
|
+
adaptiveAckRecoveryOkCount: args.adaptiveAckRecoveryOkCount,
|
|
140
|
+
nowMs: args.nowMs,
|
|
141
|
+
defaultAckTimeoutMs: args.defaultAckTimeoutMs,
|
|
142
|
+
minAckTimeoutMs: args.minAckTimeoutMs,
|
|
143
|
+
maxAckTimeoutMs: args.maxAckTimeoutMs,
|
|
144
|
+
lateAckObservationTtlMs: args.lateAckObservationTtlMs,
|
|
145
|
+
recoveryOkThreshold: args.recoveryOkThreshold,
|
|
146
|
+
});
|
|
147
|
+
return {
|
|
148
|
+
lastAckOkAt: args.lastAckOkAt,
|
|
149
|
+
lastAckTimeoutAt: args.lastAckTimeoutAt,
|
|
150
|
+
recentAckTimeoutCount: args.recentAckTimeoutCount,
|
|
151
|
+
lateAckOkCount: args.lateAckOkCount,
|
|
152
|
+
lastLateAckOkAt: args.lastLateAckOkAt,
|
|
153
|
+
lastLateAckAgeMs,
|
|
154
|
+
lateAckObservationTtlMs: args.lateAckObservationTtlMs,
|
|
155
|
+
lateAckObservationExpired,
|
|
156
|
+
adaptiveAckRecoveryOkCount: args.adaptiveAckRecoveryOkCount,
|
|
157
|
+
adaptiveAckRecoveryOkThreshold: args.recoveryOkThreshold,
|
|
158
|
+
adaptiveAckRecovered,
|
|
159
|
+
lastAckQueueLatencyMs: args.lastAckQueueLatencyMs,
|
|
160
|
+
lastAckPushLatencyMs: args.lastAckPushLatencyMs,
|
|
161
|
+
lastLateAckQueueLatencyMs: args.lastLateAckQueueLatencyMs,
|
|
162
|
+
lastLateAckPushLatencyMs: args.lastLateAckPushLatencyMs,
|
|
163
|
+
adaptiveAckTimeoutEnabled: args.adaptiveAckTimeoutEnabled,
|
|
164
|
+
defaultAckTimeoutMs: args.defaultAckTimeoutMs,
|
|
165
|
+
currentAckTimeoutMs: args.currentAckTimeoutMs,
|
|
166
|
+
recommendedAckTimeoutMs: ackTimeoutDecision.timeoutMs,
|
|
167
|
+
recommendedAckTimeoutReason: ackTimeoutDecision.reason,
|
|
168
|
+
};
|
|
169
|
+
}
|