@xmoxmo/bncr 0.3.3 → 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 +11 -10
- package/openclaw.plugin.json +21 -0
- package/package.json +4 -4
- package/scripts/check-pack.mjs +112 -22
- package/scripts/check-register-drift.mjs +91 -65
- package/scripts/selfcheck.mjs +79 -3
- package/src/channel.ts +549 -810
- package/src/core/accounts.ts +1 -1
- package/src/core/connection-capability.ts +2 -2
- package/src/core/connection-reachability.ts +112 -1
- 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 +15 -10
- package/src/core/extended-diagnostics.ts +4 -0
- package/src/core/file-transfer-payloads.ts +1 -4
- package/src/core/logging.ts +98 -0
- package/src/core/outbox-entry-builders.ts +15 -2
- package/src/core/outbox-file-transfer-bookkeeping.ts +1 -1
- package/src/core/outbox-file-transfer-failure.ts +2 -5
- package/src/core/outbox-file-transfer-success.ts +1 -4
- package/src/core/outbox-text-push-failure.ts +2 -4
- package/src/core/outbox-text-push-success.ts +1 -1
- 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 +42 -94
- package/src/messaging/inbound/dispatch.ts +25 -54
- 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/inbound/parse.ts +3 -3
- package/src/messaging/inbound/runtime-compat.ts +8 -2
- package/src/messaging/outbound/build-send-action.ts +1 -2
- package/src/messaging/outbound/diagnostics.ts +221 -2
- package/src/messaging/outbound/durable-message-adapter.ts +15 -5
- package/src/messaging/outbound/durable-queue-adapter.ts +3 -1
- package/src/messaging/outbound/media.ts +2 -1
- package/src/messaging/outbound/queue-selectors.ts +19 -6
- package/src/messaging/outbound/reasons.ts +2 -0
- package/src/messaging/outbound/reply-enqueue.ts +29 -2
- package/src/messaging/outbound/reply-target-policy.ts +4 -1
- package/src/messaging/outbound/retry-policy.ts +16 -8
- package/src/messaging/outbound/send-params.ts +56 -0
- package/src/messaging/outbound/session-route.ts +1 -1
- package/src/openclaw/reply-runtime.ts +4 -5
- package/src/openclaw/routing-runtime.ts +0 -1
- package/src/openclaw/runtime-surface.ts +29 -0
- package/src/openclaw/sdk-helpers.ts +4 -1
- package/src/plugin/gateway-methods.ts +2 -0
- package/src/plugin/messaging.ts +2 -9
- package/src/plugin/status.ts +15 -5
- package/src/runtime/outbound-ack-timeout.ts +73 -0
- package/src/runtime/outbound-flags.ts +1 -1
- package/src/runtime/outbox-transitions.ts +4 -4
- package/src/runtime/register-trace-runtime.ts +102 -0
- package/src/runtime/status-snapshots.ts +10 -4
- package/src/runtime/status-worker.ts +78 -13
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { normalizeAccountId } from './accounts.ts';
|
|
2
|
+
import { normalizeStoredSessionKey, parseRouteLike } from './targets.ts';
|
|
3
|
+
import type { OutboxEntry } from './types.ts';
|
|
4
|
+
|
|
5
|
+
function asString(v: unknown, fallback = ''): string {
|
|
6
|
+
if (typeof v === 'string') return v;
|
|
7
|
+
if (v == null) return fallback;
|
|
8
|
+
return String(v);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
12
|
+
const n = Number(value);
|
|
13
|
+
return Number.isFinite(n) ? n : fallback;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function optionalFiniteNumber(value: unknown): number | undefined {
|
|
17
|
+
if (value == null || value === '') return undefined;
|
|
18
|
+
const n = Number(value);
|
|
19
|
+
return Number.isFinite(n) ? n : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function normalizePersistedOutboxEntry(args: {
|
|
23
|
+
entry: any;
|
|
24
|
+
canonicalAgentId: string;
|
|
25
|
+
now: () => number;
|
|
26
|
+
}): OutboxEntry | null {
|
|
27
|
+
const { entry, canonicalAgentId } = args;
|
|
28
|
+
if (!entry?.messageId) return null;
|
|
29
|
+
const accountId = normalizeAccountId(entry.accountId);
|
|
30
|
+
const sessionKey = asString(entry.sessionKey || '').trim();
|
|
31
|
+
const normalized = normalizeStoredSessionKey(sessionKey, canonicalAgentId);
|
|
32
|
+
if (!normalized) return null;
|
|
33
|
+
|
|
34
|
+
const route = parseRouteLike(entry.route) || normalized.route;
|
|
35
|
+
const payload = entry.payload && typeof entry.payload === 'object' ? { ...entry.payload } : {};
|
|
36
|
+
(payload as any).sessionKey = normalized.sessionKey;
|
|
37
|
+
(payload as any).platform = route.platform;
|
|
38
|
+
(payload as any).groupId = route.groupId;
|
|
39
|
+
(payload as any).userId = route.userId;
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
...entry,
|
|
43
|
+
accountId,
|
|
44
|
+
sessionKey: normalized.sessionKey,
|
|
45
|
+
route,
|
|
46
|
+
payload,
|
|
47
|
+
createdAt: finiteNumberOr(entry.createdAt, args.now()),
|
|
48
|
+
retryCount: finiteNumberOr(entry.retryCount, 0),
|
|
49
|
+
nextAttemptAt: finiteNumberOr(entry.nextAttemptAt, args.now()),
|
|
50
|
+
lastAttemptAt: optionalFiniteNumber(entry.lastAttemptAt),
|
|
51
|
+
lastError: entry.lastError ? asString(entry.lastError) : undefined,
|
|
52
|
+
};
|
|
53
|
+
}
|
package/src/core/probe.ts
CHANGED
|
@@ -1,3 +1,19 @@
|
|
|
1
|
+
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
2
|
+
if (value == null) return fallback;
|
|
3
|
+
const n = Number(value);
|
|
4
|
+
return Number.isFinite(n) ? n : fallback;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function finiteNumberOrNull(value: unknown): number | null {
|
|
8
|
+
if (value == null) return null;
|
|
9
|
+
const n = Number(value);
|
|
10
|
+
return Number.isFinite(n) ? n : null;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function nonNegativeFiniteNumberOr(value: unknown, fallback: number): number {
|
|
14
|
+
return Math.max(0, finiteNumberOr(value, fallback));
|
|
15
|
+
}
|
|
16
|
+
|
|
1
17
|
export function probeBncrAccount(params: {
|
|
2
18
|
accountId: string;
|
|
3
19
|
connected: boolean;
|
|
@@ -14,18 +30,22 @@ export function probeBncrAccount(params: {
|
|
|
14
30
|
};
|
|
15
31
|
}) {
|
|
16
32
|
const issues: string[] = [];
|
|
33
|
+
const pending = nonNegativeFiniteNumberOr(params.pending, 0);
|
|
34
|
+
const deadLetter = nonNegativeFiniteNumberOr(params.deadLetter, 0);
|
|
35
|
+
const activeConnections = nonNegativeFiniteNumberOr(params.activeConnections, 0);
|
|
36
|
+
const invalidOutboxSessionKeys = nonNegativeFiniteNumberOr(params.invalidOutboxSessionKeys, 0);
|
|
37
|
+
const legacyAccountResidue = nonNegativeFiniteNumberOr(params.legacyAccountResidue, 0);
|
|
17
38
|
|
|
18
39
|
if (!params.connected) issues.push('not-connected');
|
|
19
|
-
if (
|
|
20
|
-
if (
|
|
21
|
-
if (
|
|
22
|
-
if (
|
|
23
|
-
if (
|
|
40
|
+
if (pending > 20) issues.push('pending-high');
|
|
41
|
+
if (deadLetter > 0) issues.push('dead-letter');
|
|
42
|
+
if (activeConnections > 3) issues.push('too-many-connections');
|
|
43
|
+
if (invalidOutboxSessionKeys > 0) issues.push('invalid-session-keys');
|
|
44
|
+
if (legacyAccountResidue > 0) issues.push('legacy-account-residue');
|
|
24
45
|
|
|
25
46
|
let level: 'ok' | 'warn' | 'error' = 'ok';
|
|
26
47
|
if (issues.length > 0) level = 'warn';
|
|
27
|
-
if (!params.connected && (
|
|
28
|
-
level = 'error';
|
|
48
|
+
if (!params.connected && (deadLetter > 0 || invalidOutboxSessionKeys > 0)) level = 'error';
|
|
29
49
|
|
|
30
50
|
return {
|
|
31
51
|
ok: level === 'ok',
|
|
@@ -34,12 +54,12 @@ export function probeBncrAccount(params: {
|
|
|
34
54
|
details: {
|
|
35
55
|
accountId: params.accountId,
|
|
36
56
|
connected: params.connected,
|
|
37
|
-
pending
|
|
38
|
-
deadLetter
|
|
39
|
-
activeConnections
|
|
40
|
-
invalidOutboxSessionKeys
|
|
41
|
-
legacyAccountResidue
|
|
42
|
-
lastActivityAt: params.lastActivityAt
|
|
57
|
+
pending,
|
|
58
|
+
deadLetter,
|
|
59
|
+
activeConnections,
|
|
60
|
+
invalidOutboxSessionKeys,
|
|
61
|
+
legacyAccountResidue,
|
|
62
|
+
lastActivityAt: finiteNumberOrNull(params.lastActivityAt),
|
|
43
63
|
structure: params.structure ?? null,
|
|
44
64
|
},
|
|
45
65
|
};
|
|
@@ -81,6 +81,54 @@ export function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
|
|
|
81
81
|
return winner;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
+
export function normalizeRegisterDriftSnapshot(raw: unknown): RegisterDriftSnapshot | null {
|
|
85
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
86
|
+
const data = raw as Record<string, unknown>;
|
|
87
|
+
const finiteOrNull = (value: unknown): number | null => {
|
|
88
|
+
const n = Number(value);
|
|
89
|
+
return Number.isFinite(n) ? n : null;
|
|
90
|
+
};
|
|
91
|
+
const cleanString = (value: unknown): string | null => {
|
|
92
|
+
const text = value == null ? '' : String(value);
|
|
93
|
+
return text.trim() || null;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
capturedAt: finiteNumberOr(data.capturedAt, 0),
|
|
98
|
+
registerCount: finiteOrNull(data.registerCount),
|
|
99
|
+
apiGeneration: finiteOrNull(data.apiGeneration),
|
|
100
|
+
postWarmupRegisterCount: finiteOrNull(data.postWarmupRegisterCount),
|
|
101
|
+
apiInstanceId: cleanString(data.apiInstanceId),
|
|
102
|
+
registryFingerprint: cleanString(data.registryFingerprint),
|
|
103
|
+
dominantBucket: cleanString(data.dominantBucket),
|
|
104
|
+
sourceBuckets:
|
|
105
|
+
data.sourceBuckets && typeof data.sourceBuckets === 'object'
|
|
106
|
+
? { ...(data.sourceBuckets as Record<string, number>) }
|
|
107
|
+
: {},
|
|
108
|
+
traceWindowSize: finiteNumberOr(data.traceWindowSize, 0),
|
|
109
|
+
traceRecent: Array.isArray(data.traceRecent)
|
|
110
|
+
? [...(data.traceRecent as Array<Record<string, unknown>>)]
|
|
111
|
+
: [],
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function dumpRegisterDriftSnapshot(snapshot: RegisterDriftSnapshot | null) {
|
|
116
|
+
return snapshot
|
|
117
|
+
? {
|
|
118
|
+
capturedAt: snapshot.capturedAt,
|
|
119
|
+
registerCount: snapshot.registerCount,
|
|
120
|
+
apiGeneration: snapshot.apiGeneration,
|
|
121
|
+
postWarmupRegisterCount: snapshot.postWarmupRegisterCount,
|
|
122
|
+
apiInstanceId: snapshot.apiInstanceId,
|
|
123
|
+
registryFingerprint: snapshot.registryFingerprint,
|
|
124
|
+
dominantBucket: snapshot.dominantBucket,
|
|
125
|
+
sourceBuckets: { ...snapshot.sourceBuckets },
|
|
126
|
+
traceWindowSize: snapshot.traceWindowSize,
|
|
127
|
+
traceRecent: snapshot.traceRecent.map((trace) => ({ ...trace })),
|
|
128
|
+
}
|
|
129
|
+
: null;
|
|
130
|
+
}
|
|
131
|
+
|
|
84
132
|
export function buildRegisterTraceEntry(args: {
|
|
85
133
|
ts: number;
|
|
86
134
|
bridgeId: string;
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { PendingAdmission } from './types.ts';
|
|
2
|
+
|
|
3
|
+
type RuntimeStatusMetaDisplayInput = {
|
|
4
|
+
pendingAdmissions?: PendingAdmission[];
|
|
5
|
+
lastSession?: { scope: string; updatedAt: number } | null;
|
|
6
|
+
lastActivityAt?: number | null;
|
|
7
|
+
lastInboundAt?: number | null;
|
|
8
|
+
lastOutboundAt?: number | null;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
function now() {
|
|
12
|
+
return Date.now();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function finiteNumberOrNull(value: unknown): number | null {
|
|
16
|
+
const n = Number(value);
|
|
17
|
+
return Number.isFinite(n) ? n : null;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function formatPendingAdmissionScope(route: unknown): string | null {
|
|
21
|
+
if (!route || typeof route !== 'object') return null;
|
|
22
|
+
const record = route as Record<string, unknown>;
|
|
23
|
+
if (
|
|
24
|
+
typeof record.platform !== 'string' ||
|
|
25
|
+
typeof record.groupId !== 'string' ||
|
|
26
|
+
typeof record.userId !== 'string'
|
|
27
|
+
) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
return `${record.platform}:${record.groupId}:${record.userId}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function fmtAgo(ts?: number | null): string {
|
|
34
|
+
if (!ts || !Number.isFinite(ts) || ts <= 0) return '-';
|
|
35
|
+
const diff = Math.max(0, now() - ts);
|
|
36
|
+
if (diff < 1_000) return 'just now';
|
|
37
|
+
if (diff < 60_000) return `${Math.floor(diff / 1_000)}s ago`;
|
|
38
|
+
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
39
|
+
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
40
|
+
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function buildRuntimeStatusMetaDisplay(input: RuntimeStatusMetaDisplayInput) {
|
|
44
|
+
const lastSessionAt = finiteNumberOrNull(input.lastSession?.updatedAt);
|
|
45
|
+
const lastActivityAt = finiteNumberOrNull(input.lastActivityAt);
|
|
46
|
+
const lastInboundAt = finiteNumberOrNull(input.lastInboundAt);
|
|
47
|
+
const lastOutboundAt = finiteNumberOrNull(input.lastOutboundAt);
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
pendingAdmissionsCount: Array.isArray(input.pendingAdmissions)
|
|
51
|
+
? input.pendingAdmissions.length
|
|
52
|
+
: 0,
|
|
53
|
+
pendingAdmissions: Array.isArray(input.pendingAdmissions)
|
|
54
|
+
? input.pendingAdmissions.map((item) => ({
|
|
55
|
+
clientId: item.clientId,
|
|
56
|
+
scope: formatPendingAdmissionScope(item.route),
|
|
57
|
+
scopes: Array.isArray(item.routes)
|
|
58
|
+
? item.routes
|
|
59
|
+
.map((route) => formatPendingAdmissionScope(route))
|
|
60
|
+
.filter((scope): scope is string => scope !== null)
|
|
61
|
+
: [],
|
|
62
|
+
firstSeenAt: item.firstSeenAt,
|
|
63
|
+
lastSeenAt: item.lastSeenAt,
|
|
64
|
+
attempts: item.attempts,
|
|
65
|
+
}))
|
|
66
|
+
: [],
|
|
67
|
+
lastSessionScope: input.lastSession?.scope || null,
|
|
68
|
+
lastSessionAt,
|
|
69
|
+
lastSessionAgo: fmtAgo(lastSessionAt),
|
|
70
|
+
lastActivityAt,
|
|
71
|
+
lastActivityAgo: fmtAgo(lastActivityAt),
|
|
72
|
+
lastInboundAt,
|
|
73
|
+
lastInboundAgo: fmtAgo(lastInboundAt),
|
|
74
|
+
lastOutboundAt,
|
|
75
|
+
lastOutboundAgo: fmtAgo(lastOutboundAt),
|
|
76
|
+
};
|
|
77
|
+
}
|
package/src/core/status.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { buildRuntimeStatusMetaDisplay } from './status-meta.ts';
|
|
3
4
|
import type { BncrDiagnosticsSummary, PendingAdmission } from './types.ts';
|
|
4
5
|
|
|
5
6
|
type RuntimeStatusInput = {
|
|
@@ -35,44 +36,50 @@ function finiteNumberOr(value: unknown, fallback: number): number {
|
|
|
35
36
|
return Number.isFinite(n) ? n : fallback;
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
function
|
|
39
|
-
|
|
40
|
-
const diff = Math.max(0, now() - ts);
|
|
41
|
-
if (diff < 1_000) return 'just now';
|
|
42
|
-
if (diff < 60_000) return `${Math.floor(diff / 1_000)}s ago`;
|
|
43
|
-
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
44
|
-
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
45
|
-
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
39
|
+
function nonNegativeFiniteNumberOr(value: unknown, fallback: number): number {
|
|
40
|
+
return Math.max(0, finiteNumberOr(value, fallback));
|
|
46
41
|
}
|
|
47
42
|
|
|
48
43
|
export function buildIntegratedDiagnostics(input: RuntimeStatusInput): BncrDiagnosticsSummary {
|
|
49
44
|
const root = input.channelRoot || path.join(process.cwd(), 'plugins', 'bncr');
|
|
50
45
|
const pluginIndexExists = fs.existsSync(path.join(root, 'index.ts'));
|
|
51
46
|
const pluginChannelExists = fs.existsSync(path.join(root, 'src', 'channel.ts'));
|
|
47
|
+
const currentTime = now();
|
|
48
|
+
const startedAt = finiteNumberOr(input.startedAt, currentTime);
|
|
49
|
+
const pending = nonNegativeFiniteNumberOr(input.pending, 0);
|
|
50
|
+
const deadLetter = nonNegativeFiniteNumberOr(input.deadLetter, 0);
|
|
51
|
+
const activeConnections = nonNegativeFiniteNumberOr(input.activeConnections, 0);
|
|
52
|
+
const connectEvents = nonNegativeFiniteNumberOr(input.connectEvents, 0);
|
|
53
|
+
const inboundEvents = nonNegativeFiniteNumberOr(input.inboundEvents, 0);
|
|
54
|
+
const activityEvents = nonNegativeFiniteNumberOr(input.activityEvents, 0);
|
|
55
|
+
const ackEvents = nonNegativeFiniteNumberOr(input.ackEvents, 0);
|
|
56
|
+
const sessionRoutesCount = nonNegativeFiniteNumberOr(input.sessionRoutesCount, 0);
|
|
57
|
+
const invalidOutboxSessionKeys = nonNegativeFiniteNumberOr(input.invalidOutboxSessionKeys, 0);
|
|
58
|
+
const legacyAccountResidue = nonNegativeFiniteNumberOr(input.legacyAccountResidue, 0);
|
|
52
59
|
|
|
53
60
|
return {
|
|
54
61
|
health: {
|
|
55
62
|
connected: input.connected,
|
|
56
|
-
pending
|
|
63
|
+
pending,
|
|
57
64
|
pendingAdmissions: Array.isArray(input.pendingAdmissions)
|
|
58
65
|
? input.pendingAdmissions.length
|
|
59
66
|
: 0,
|
|
60
|
-
deadLetter
|
|
61
|
-
activeConnections
|
|
62
|
-
connectEvents
|
|
63
|
-
inboundEvents
|
|
64
|
-
activityEvents
|
|
65
|
-
ackEvents
|
|
66
|
-
uptimeSec: Math.floor((
|
|
67
|
+
deadLetter,
|
|
68
|
+
activeConnections,
|
|
69
|
+
connectEvents,
|
|
70
|
+
inboundEvents,
|
|
71
|
+
activityEvents,
|
|
72
|
+
ackEvents,
|
|
73
|
+
uptimeSec: Math.max(0, Math.floor((currentTime - startedAt) / 1000)),
|
|
67
74
|
},
|
|
68
75
|
regression: {
|
|
69
76
|
pluginFilesPresent: pluginIndexExists && pluginChannelExists,
|
|
70
77
|
pluginIndexExists,
|
|
71
78
|
pluginChannelExists,
|
|
72
|
-
totalKnownRoutes:
|
|
73
|
-
invalidOutboxSessionKeys
|
|
74
|
-
legacyAccountResidue
|
|
75
|
-
ok:
|
|
79
|
+
totalKnownRoutes: sessionRoutesCount,
|
|
80
|
+
invalidOutboxSessionKeys,
|
|
81
|
+
legacyAccountResidue,
|
|
82
|
+
ok: invalidOutboxSessionKeys === 0 && legacyAccountResidue === 0,
|
|
76
83
|
},
|
|
77
84
|
};
|
|
78
85
|
}
|
|
@@ -91,35 +98,21 @@ export function buildStatusHeadlineFromRuntime(input: RuntimeStatusInput): strin
|
|
|
91
98
|
|
|
92
99
|
export function buildStatusMetaFromRuntime(input: RuntimeStatusInput) {
|
|
93
100
|
const diagnostics = buildIntegratedDiagnostics(input);
|
|
101
|
+
const display = buildRuntimeStatusMetaDisplay(input);
|
|
94
102
|
return {
|
|
95
|
-
pending:
|
|
96
|
-
pendingAdmissionsCount:
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
firstSeenAt: item.firstSeenAt,
|
|
109
|
-
lastSeenAt: item.lastSeenAt,
|
|
110
|
-
attempts: item.attempts,
|
|
111
|
-
}))
|
|
112
|
-
: [],
|
|
113
|
-
deadLetter: input.deadLetter,
|
|
114
|
-
lastSessionScope: input.lastSession?.scope || null,
|
|
115
|
-
lastSessionAt: input.lastSession?.updatedAt || null,
|
|
116
|
-
lastSessionAgo: fmtAgo(input.lastSession?.updatedAt || null),
|
|
117
|
-
lastActivityAt: input.lastActivityAt || null,
|
|
118
|
-
lastActivityAgo: fmtAgo(input.lastActivityAt || null),
|
|
119
|
-
lastInboundAt: input.lastInboundAt || null,
|
|
120
|
-
lastInboundAgo: fmtAgo(input.lastInboundAt || null),
|
|
121
|
-
lastOutboundAt: input.lastOutboundAt || null,
|
|
122
|
-
lastOutboundAgo: fmtAgo(input.lastOutboundAt || null),
|
|
103
|
+
pending: diagnostics.health.pending,
|
|
104
|
+
pendingAdmissionsCount: display.pendingAdmissionsCount,
|
|
105
|
+
pendingAdmissions: display.pendingAdmissions,
|
|
106
|
+
deadLetter: diagnostics.health.deadLetter,
|
|
107
|
+
lastSessionScope: display.lastSessionScope,
|
|
108
|
+
lastSessionAt: display.lastSessionAt,
|
|
109
|
+
lastSessionAgo: display.lastSessionAgo,
|
|
110
|
+
lastActivityAt: display.lastActivityAt,
|
|
111
|
+
lastActivityAgo: display.lastActivityAgo,
|
|
112
|
+
lastInboundAt: display.lastInboundAt,
|
|
113
|
+
lastInboundAgo: display.lastInboundAgo,
|
|
114
|
+
lastOutboundAt: display.lastOutboundAt,
|
|
115
|
+
lastOutboundAgo: display.lastOutboundAgo,
|
|
123
116
|
diagnostics,
|
|
124
117
|
};
|
|
125
118
|
}
|
|
@@ -131,17 +124,17 @@ export function buildAccountRuntimeSnapshot(input: RuntimeStatusInput) {
|
|
|
131
124
|
running: input.running ?? true,
|
|
132
125
|
connected: input.connected,
|
|
133
126
|
linked: input.connected,
|
|
134
|
-
lastEventAt:
|
|
135
|
-
lastInboundAt:
|
|
136
|
-
lastOutboundAt:
|
|
127
|
+
lastEventAt: meta.lastActivityAt,
|
|
128
|
+
lastInboundAt: meta.lastInboundAt,
|
|
129
|
+
lastOutboundAt: meta.lastOutboundAt,
|
|
137
130
|
mode: input.connected ? 'linked' : 'configured',
|
|
138
131
|
lastError: input.lastError ?? null,
|
|
139
|
-
pending:
|
|
140
|
-
deadLetter:
|
|
132
|
+
pending: meta.pending,
|
|
133
|
+
deadLetter: meta.deadLetter,
|
|
141
134
|
lastSessionKey: input.lastSession?.sessionKey || null,
|
|
142
135
|
lastSessionScope: input.lastSession?.scope || null,
|
|
143
|
-
lastSessionAt:
|
|
144
|
-
lastActivityAt:
|
|
136
|
+
lastSessionAt: meta.lastSessionAt,
|
|
137
|
+
lastActivityAt: meta.lastActivityAt,
|
|
145
138
|
diagnostics: meta.diagnostics,
|
|
146
139
|
meta,
|
|
147
140
|
};
|
|
@@ -156,8 +149,8 @@ export function buildAccountStatusSnapshot(input: {
|
|
|
156
149
|
const rt = input.runtime || {};
|
|
157
150
|
const meta = rt?.meta || {};
|
|
158
151
|
|
|
159
|
-
const pending =
|
|
160
|
-
const deadLetter =
|
|
152
|
+
const pending = nonNegativeFiniteNumberOr(rt?.pending ?? meta.pending, 0);
|
|
153
|
+
const deadLetter = nonNegativeFiniteNumberOr(rt?.deadLetter ?? meta.deadLetter, 0);
|
|
161
154
|
const lastSessionKey = rt?.lastSessionKey ?? meta.lastSessionKey ?? null;
|
|
162
155
|
const lastSessionScope = rt?.lastSessionScope ?? meta.lastSessionScope ?? null;
|
|
163
156
|
const lastSessionAt = rt?.lastSessionAt ?? meta.lastSessionAt ?? null;
|
|
@@ -5,6 +5,17 @@ import {
|
|
|
5
5
|
normalizeInboundSessionKey,
|
|
6
6
|
withTaskSessionKey,
|
|
7
7
|
} from '../../core/targets.ts';
|
|
8
|
+
import {
|
|
9
|
+
recordBncrInboundSession,
|
|
10
|
+
resolveBncrInboundSessionStorePath,
|
|
11
|
+
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
12
|
+
} from '../../openclaw/inbound-session-runtime.ts';
|
|
13
|
+
import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
|
|
14
|
+
import { resolveOpenClawAgentRoute } from '../../openclaw/routing-runtime.ts';
|
|
15
|
+
import type { OutboundReplyTargetPolicy } from '../outbound/reply-target-policy.ts';
|
|
16
|
+
import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
|
|
17
|
+
import { parseBncrNativeCommand, resolveBncrNativeVerboseCommand } from './native-command.ts';
|
|
18
|
+
import { buildBncrNativeReplyDeliveryPayload } from './native-reply-delivery.ts';
|
|
8
19
|
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
9
20
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
10
21
|
import {
|
|
@@ -12,43 +23,9 @@ import {
|
|
|
12
23
|
recordAndPatchBncrInboundSessionEntry,
|
|
13
24
|
wrapBncrInboundRecordSessionLabelCorrection,
|
|
14
25
|
} from './session-label.ts';
|
|
15
|
-
import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
|
|
16
|
-
import {
|
|
17
|
-
resolveOpenClawAgentRoute,
|
|
18
|
-
resolveOpenClawInboundLastRouteSessionKey,
|
|
19
|
-
} from '../../openclaw/routing-runtime.ts';
|
|
20
|
-
import {
|
|
21
|
-
recordBncrInboundSession,
|
|
22
|
-
resolveBncrInboundSessionStorePath,
|
|
23
|
-
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
24
|
-
} from '../../openclaw/inbound-session-runtime.ts';
|
|
25
26
|
|
|
26
27
|
type ParsedInbound = ReturnType<typeof import('./parse.ts')['parseBncrInboundParams']>;
|
|
27
28
|
|
|
28
|
-
type NativeCommand = {
|
|
29
|
-
command: string;
|
|
30
|
-
raw: string;
|
|
31
|
-
body: string;
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type NativeVerboseCommand = {
|
|
35
|
-
handled: true;
|
|
36
|
-
verboseLevel?: 'on' | 'off' | 'full';
|
|
37
|
-
text: string;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
function resolveBncrNativeVerboseCommand(command: NativeCommand): NativeVerboseCommand | null {
|
|
41
|
-
if (command.command !== 'verbose') return null;
|
|
42
|
-
const rawLevel = String(command.raw.slice('/verbose'.length) || '').trim().toLowerCase();
|
|
43
|
-
if (!rawLevel || rawLevel === 'status') {
|
|
44
|
-
return { handled: true, text: 'Current verbose level is unchanged.' };
|
|
45
|
-
}
|
|
46
|
-
if (rawLevel === 'on') return { handled: true, verboseLevel: 'on', text: 'Verbose logging enabled.' };
|
|
47
|
-
if (rawLevel === 'off') return { handled: true, verboseLevel: 'off', text: 'Verbose logging disabled.' };
|
|
48
|
-
if (rawLevel === 'full') return { handled: true, verboseLevel: 'full', text: 'Verbose logging set to full.' };
|
|
49
|
-
return { handled: true, text: `Unrecognized verbose level "${rawLevel}". Valid levels: off, on, full.` };
|
|
50
|
-
}
|
|
51
|
-
|
|
52
29
|
function logBncrNativeCommandEvent(
|
|
53
30
|
event: string,
|
|
54
31
|
fields: Record<string, unknown>,
|
|
@@ -58,21 +35,7 @@ function logBncrNativeCommandEvent(
|
|
|
58
35
|
emitBncrLogLine('info', `[bncr] native-command ${JSON.stringify({ event, ...fields })}`);
|
|
59
36
|
}
|
|
60
37
|
|
|
61
|
-
export
|
|
62
|
-
const raw = String(text || '').trim();
|
|
63
|
-
if (!raw.startsWith('/')) return null;
|
|
64
|
-
const match = raw.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/i);
|
|
65
|
-
if (!match) return null;
|
|
66
|
-
|
|
67
|
-
const command = String(match[1] || '')
|
|
68
|
-
.trim()
|
|
69
|
-
.toLowerCase();
|
|
70
|
-
if (!command) return null;
|
|
71
|
-
|
|
72
|
-
const rest = String(match[2] || '').trim();
|
|
73
|
-
const body = command === 'help' ? ['/commands', rest].filter(Boolean).join(' ') : raw;
|
|
74
|
-
return { command, raw, body };
|
|
75
|
-
}
|
|
38
|
+
export { parseBncrNativeCommand } from './native-command.ts';
|
|
76
39
|
|
|
77
40
|
export async function handleBncrNativeCommand(params: {
|
|
78
41
|
api: any;
|
|
@@ -87,23 +50,25 @@ export async function handleBncrNativeCommand(params: {
|
|
|
87
50
|
route: any;
|
|
88
51
|
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
89
52
|
mediaLocalRoots?: readonly string[];
|
|
53
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
90
54
|
}) => Promise<void>;
|
|
91
55
|
logger?: { warn?: (msg: string) => void; error?: (msg: string) => void };
|
|
92
56
|
}): Promise<
|
|
93
57
|
| { handled: false }
|
|
94
58
|
| { handled: true; command: string; sessionKey: string; fallbackToAgent?: boolean }
|
|
95
59
|
> {
|
|
60
|
+
const { api, channelId, cfg, parsed, canonicalAgentId, rememberSessionRoute, enqueueFromReply } =
|
|
61
|
+
params;
|
|
96
62
|
const {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
} =
|
|
106
|
-
const { accountId, route, peer, sessionKeyfromroute, providedOriginatingTo, clientId, extracted, msgId } = parsed;
|
|
63
|
+
accountId,
|
|
64
|
+
route,
|
|
65
|
+
peer,
|
|
66
|
+
sessionKeyfromroute,
|
|
67
|
+
providedOriginatingTo,
|
|
68
|
+
clientId,
|
|
69
|
+
extracted,
|
|
70
|
+
msgId,
|
|
71
|
+
} = parsed;
|
|
107
72
|
const command = parseBncrNativeCommand(extracted.text);
|
|
108
73
|
if (!command) return { handled: false };
|
|
109
74
|
const nativeCommandDebugEnabled = cfg?.channels?.[channelId]?.debug?.verbose === true;
|
|
@@ -275,9 +240,15 @@ export async function handleBncrNativeCommand(params: {
|
|
|
275
240
|
normalizeEntry: (entry: string) => String(entry || '').trim(),
|
|
276
241
|
})
|
|
277
242
|
: null;
|
|
278
|
-
const
|
|
279
|
-
|
|
243
|
+
const updateLastRoute = buildBncrInboundRecordUpdateLastRoute({
|
|
244
|
+
channelId,
|
|
245
|
+
peerKind: peer.kind,
|
|
246
|
+
senderIdForContext,
|
|
247
|
+
accountId,
|
|
248
|
+
to: displayTo,
|
|
249
|
+
resolvedRoute,
|
|
280
250
|
sessionKey,
|
|
251
|
+
pinnedMainDmOwner,
|
|
281
252
|
});
|
|
282
253
|
|
|
283
254
|
let responded = false;
|
|
@@ -316,22 +287,7 @@ export async function handleBncrNativeCommand(params: {
|
|
|
316
287
|
expectedLabel: displayTo,
|
|
317
288
|
}),
|
|
318
289
|
record: {
|
|
319
|
-
updateLastRoute
|
|
320
|
-
peer.kind === 'direct'
|
|
321
|
-
? {
|
|
322
|
-
sessionKey: inboundLastRouteSessionKey,
|
|
323
|
-
channel: channelId,
|
|
324
|
-
to: displayTo,
|
|
325
|
-
accountId,
|
|
326
|
-
mainDmOwnerPin:
|
|
327
|
-
inboundLastRouteSessionKey === resolvedRoute.mainSessionKey && pinnedMainDmOwner
|
|
328
|
-
? {
|
|
329
|
-
ownerRecipient: pinnedMainDmOwner,
|
|
330
|
-
senderRecipient: senderIdForContext,
|
|
331
|
-
}
|
|
332
|
-
: undefined,
|
|
333
|
-
}
|
|
334
|
-
: undefined,
|
|
290
|
+
updateLastRoute,
|
|
335
291
|
onRecordError: (err: unknown) => {
|
|
336
292
|
emitBncrLogLine(
|
|
337
293
|
'warn',
|
|
@@ -354,18 +310,13 @@ export async function handleBncrNativeCommand(params: {
|
|
|
354
310
|
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
355
311
|
) => {
|
|
356
312
|
const kind = info?.kind;
|
|
357
|
-
const
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
payload?.text ||
|
|
365
|
-
payload?.mediaUrl ||
|
|
366
|
-
(Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
|
|
367
|
-
);
|
|
368
|
-
if (!hasPayload) return;
|
|
313
|
+
const deliveryPayload = buildBncrNativeReplyDeliveryPayload({
|
|
314
|
+
payload,
|
|
315
|
+
kind,
|
|
316
|
+
effectiveReply,
|
|
317
|
+
msgId,
|
|
318
|
+
});
|
|
319
|
+
if (!deliveryPayload) return;
|
|
369
320
|
if (!responded) {
|
|
370
321
|
logBncrNativeCommandEvent(
|
|
371
322
|
'payload-produced',
|
|
@@ -386,11 +337,8 @@ export async function handleBncrNativeCommand(params: {
|
|
|
386
337
|
accountId,
|
|
387
338
|
sessionKey,
|
|
388
339
|
route,
|
|
389
|
-
payload:
|
|
390
|
-
|
|
391
|
-
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
392
|
-
replyToId: msgId || undefined,
|
|
393
|
-
},
|
|
340
|
+
payload: deliveryPayload,
|
|
341
|
+
replyTargetPolicy: 'preserve',
|
|
394
342
|
});
|
|
395
343
|
},
|
|
396
344
|
},
|