@xmoxmo/bncr 0.3.4 → 0.3.5
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 +24 -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,6 +75,7 @@ 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
|
|
|
@@ -85,6 +91,7 @@ export function buildReplyTextOutboxEntry(
|
|
|
85
91
|
text: string;
|
|
86
92
|
kind?: 'tool' | 'block' | 'final';
|
|
87
93
|
replyToId: string;
|
|
94
|
+
replyTargetPolicy: OutboundReplyTargetPolicy;
|
|
88
95
|
},
|
|
89
96
|
helpers: {
|
|
90
97
|
buildTextOutboxEntry: (args: {
|
|
@@ -94,6 +101,7 @@ export function buildReplyTextOutboxEntry(
|
|
|
94
101
|
text: string;
|
|
95
102
|
kind?: 'tool' | 'block' | 'final';
|
|
96
103
|
replyToId?: string;
|
|
104
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
97
105
|
}) => OutboxEntry;
|
|
98
106
|
},
|
|
99
107
|
): OutboxEntry {
|
|
@@ -104,6 +112,7 @@ export function buildReplyTextOutboxEntry(
|
|
|
104
112
|
text: params.text,
|
|
105
113
|
kind: params.kind,
|
|
106
114
|
replyToId: params.replyToId || undefined,
|
|
115
|
+
replyTargetPolicy: params.replyTargetPolicy,
|
|
107
116
|
});
|
|
108
117
|
}
|
|
109
118
|
|
|
@@ -123,6 +132,7 @@ export function enqueueReplyTextEntry(
|
|
|
123
132
|
text: string;
|
|
124
133
|
kind?: 'tool' | 'block' | 'final';
|
|
125
134
|
replyToId?: string;
|
|
135
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
126
136
|
}) => OutboxEntry;
|
|
127
137
|
},
|
|
128
138
|
): void {
|
|
@@ -137,6 +147,7 @@ export function enqueueReplyTextEntry(
|
|
|
137
147
|
text: params.payload.text,
|
|
138
148
|
kind: params.payload.kind,
|
|
139
149
|
replyToId: params.payload.replyToId,
|
|
150
|
+
replyTargetPolicy: params.payload.replyTargetPolicy,
|
|
140
151
|
},
|
|
141
152
|
{ buildTextOutboxEntry: helpers.buildTextOutboxEntry },
|
|
142
153
|
),
|
|
@@ -159,6 +170,7 @@ export function enqueueReplyMediaFallbackTextEntry(
|
|
|
159
170
|
text: string;
|
|
160
171
|
kind?: 'tool' | 'block' | 'final';
|
|
161
172
|
replyToId?: string;
|
|
173
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
162
174
|
}) => OutboxEntry;
|
|
163
175
|
},
|
|
164
176
|
): void {
|
|
@@ -176,6 +188,7 @@ export function enqueueReplyMediaFallbackTextEntry(
|
|
|
176
188
|
text: params.fallback.text,
|
|
177
189
|
kind: params.kind,
|
|
178
190
|
replyToId: params.replyToId,
|
|
191
|
+
replyTargetPolicy: params.replyTargetPolicy,
|
|
179
192
|
},
|
|
180
193
|
{ buildTextOutboxEntry: helpers.buildTextOutboxEntry },
|
|
181
194
|
),
|
|
@@ -197,6 +210,7 @@ export function enqueueReplyMediaFileTransferEntry(
|
|
|
197
210
|
audioAsVoice: boolean;
|
|
198
211
|
kind?: 'tool' | 'block' | 'final';
|
|
199
212
|
replyToId?: string;
|
|
213
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
200
214
|
}) => OutboxEntry;
|
|
201
215
|
rememberRecentMediaSend: (args: {
|
|
202
216
|
sessionKey: string;
|
|
@@ -219,6 +233,7 @@ export function enqueueReplyMediaFileTransferEntry(
|
|
|
219
233
|
audioAsVoice: params.audioAsVoice,
|
|
220
234
|
kind: params.kind,
|
|
221
235
|
replyToId: params.replyToId || undefined,
|
|
236
|
+
replyTargetPolicy: params.replyTargetPolicy,
|
|
222
237
|
}),
|
|
223
238
|
);
|
|
224
239
|
helpers.rememberRecentMediaSend({
|
|
@@ -245,6 +260,7 @@ export function enqueueSingleReplyMediaEntry(
|
|
|
245
260
|
mediaUrl: params.mediaUrl,
|
|
246
261
|
kind: params.params.payload.kind,
|
|
247
262
|
replyToId: params.params.payload.replyToId,
|
|
263
|
+
replyTargetPolicy: params.params.payload.replyTargetPolicy,
|
|
248
264
|
fallback: params.fallback,
|
|
249
265
|
});
|
|
250
266
|
return;
|
|
@@ -262,6 +278,7 @@ export function enqueueSingleReplyMediaEntry(
|
|
|
262
278
|
audioAsVoice: params.params.payload.audioAsVoice,
|
|
263
279
|
kind: params.params.payload.kind,
|
|
264
280
|
replyToId: params.params.payload.replyToId,
|
|
281
|
+
replyTargetPolicy: params.params.payload.replyTargetPolicy,
|
|
265
282
|
createdAt: params.currentTime,
|
|
266
283
|
});
|
|
267
284
|
}
|
|
@@ -314,6 +331,7 @@ export function enqueueNormalizedReplyPayload(
|
|
|
314
331
|
export function normalizeReplyPayload(
|
|
315
332
|
payload: ReplyPayloadInput,
|
|
316
333
|
helpers: { asString: (value: unknown, fallback?: string) => string },
|
|
334
|
+
options?: { replyTargetPolicy?: OutboundReplyTargetPolicy },
|
|
317
335
|
): NormalizedReplyPayload {
|
|
318
336
|
const text = helpers.asString(payload?.text || '').trim();
|
|
319
337
|
const mediaUrl = helpers.asString(payload?.mediaUrl || '').trim();
|
|
@@ -328,6 +346,11 @@ export function normalizeReplyPayload(
|
|
|
328
346
|
asVoice: payload?.asVoice === true,
|
|
329
347
|
audioAsVoice: payload?.audioAsVoice === true,
|
|
330
348
|
kind: payload?.kind,
|
|
331
|
-
|
|
349
|
+
replyTargetPolicy: options?.replyTargetPolicy ?? 'agent-default',
|
|
350
|
+
replyToId: normalizeOutboundReplyToId({
|
|
351
|
+
kind: payload?.kind,
|
|
352
|
+
replyToId: payload?.replyToId,
|
|
353
|
+
replyTargetPolicy: options?.replyTargetPolicy,
|
|
354
|
+
}),
|
|
332
355
|
};
|
|
333
356
|
}
|
|
@@ -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
|
+
}
|