@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
|
@@ -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;
|
|
@@ -11,10 +11,11 @@ import {
|
|
|
11
11
|
resolveBncrPinnedMainDmOwnerFromAllowlist,
|
|
12
12
|
} from '../../openclaw/inbound-session-runtime.ts';
|
|
13
13
|
import { dispatchOpenClawReplyWithBufferedBlockDispatcher } from '../../openclaw/reply-runtime.ts';
|
|
14
|
-
import {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
} from '
|
|
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';
|
|
18
19
|
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
19
20
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
20
21
|
import {
|
|
@@ -25,38 +26,6 @@ import {
|
|
|
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) || '')
|
|
43
|
-
.trim()
|
|
44
|
-
.toLowerCase();
|
|
45
|
-
if (!rawLevel || rawLevel === 'status') {
|
|
46
|
-
return { handled: true, text: 'Current verbose level is unchanged.' };
|
|
47
|
-
}
|
|
48
|
-
if (rawLevel === 'on')
|
|
49
|
-
return { handled: true, verboseLevel: 'on', text: 'Verbose logging enabled.' };
|
|
50
|
-
if (rawLevel === 'off')
|
|
51
|
-
return { handled: true, verboseLevel: 'off', text: 'Verbose logging disabled.' };
|
|
52
|
-
if (rawLevel === 'full')
|
|
53
|
-
return { handled: true, verboseLevel: 'full', text: 'Verbose logging set to full.' };
|
|
54
|
-
return {
|
|
55
|
-
handled: true,
|
|
56
|
-
text: `Unrecognized verbose level "${rawLevel}". Valid levels: off, on, full.`,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
29
|
function logBncrNativeCommandEvent(
|
|
61
30
|
event: string,
|
|
62
31
|
fields: Record<string, unknown>,
|
|
@@ -66,21 +35,7 @@ function logBncrNativeCommandEvent(
|
|
|
66
35
|
emitBncrLogLine('info', `[bncr] native-command ${JSON.stringify({ event, ...fields })}`);
|
|
67
36
|
}
|
|
68
37
|
|
|
69
|
-
export
|
|
70
|
-
const raw = String(text || '').trim();
|
|
71
|
-
if (!raw.startsWith('/')) return null;
|
|
72
|
-
const match = raw.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/i);
|
|
73
|
-
if (!match) return null;
|
|
74
|
-
|
|
75
|
-
const command = String(match[1] || '')
|
|
76
|
-
.trim()
|
|
77
|
-
.toLowerCase();
|
|
78
|
-
if (!command) return null;
|
|
79
|
-
|
|
80
|
-
const rest = String(match[2] || '').trim();
|
|
81
|
-
const body = command === 'help' ? ['/commands', rest].filter(Boolean).join(' ') : raw;
|
|
82
|
-
return { command, raw, body };
|
|
83
|
-
}
|
|
38
|
+
export { parseBncrNativeCommand } from './native-command.ts';
|
|
84
39
|
|
|
85
40
|
export async function handleBncrNativeCommand(params: {
|
|
86
41
|
api: any;
|
|
@@ -95,6 +50,7 @@ export async function handleBncrNativeCommand(params: {
|
|
|
95
50
|
route: any;
|
|
96
51
|
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
97
52
|
mediaLocalRoots?: readonly string[];
|
|
53
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
98
54
|
}) => Promise<void>;
|
|
99
55
|
logger?: { warn?: (msg: string) => void; error?: (msg: string) => void };
|
|
100
56
|
}): Promise<
|
|
@@ -284,9 +240,15 @@ export async function handleBncrNativeCommand(params: {
|
|
|
284
240
|
normalizeEntry: (entry: string) => String(entry || '').trim(),
|
|
285
241
|
})
|
|
286
242
|
: null;
|
|
287
|
-
const
|
|
288
|
-
|
|
243
|
+
const updateLastRoute = buildBncrInboundRecordUpdateLastRoute({
|
|
244
|
+
channelId,
|
|
245
|
+
peerKind: peer.kind,
|
|
246
|
+
senderIdForContext,
|
|
247
|
+
accountId,
|
|
248
|
+
to: displayTo,
|
|
249
|
+
resolvedRoute,
|
|
289
250
|
sessionKey,
|
|
251
|
+
pinnedMainDmOwner,
|
|
290
252
|
});
|
|
291
253
|
|
|
292
254
|
let responded = false;
|
|
@@ -325,22 +287,7 @@ export async function handleBncrNativeCommand(params: {
|
|
|
325
287
|
expectedLabel: displayTo,
|
|
326
288
|
}),
|
|
327
289
|
record: {
|
|
328
|
-
updateLastRoute
|
|
329
|
-
peer.kind === 'direct'
|
|
330
|
-
? {
|
|
331
|
-
sessionKey: inboundLastRouteSessionKey,
|
|
332
|
-
channel: channelId,
|
|
333
|
-
to: displayTo,
|
|
334
|
-
accountId,
|
|
335
|
-
mainDmOwnerPin:
|
|
336
|
-
inboundLastRouteSessionKey === resolvedRoute.mainSessionKey && pinnedMainDmOwner
|
|
337
|
-
? {
|
|
338
|
-
ownerRecipient: pinnedMainDmOwner,
|
|
339
|
-
senderRecipient: senderIdForContext,
|
|
340
|
-
}
|
|
341
|
-
: undefined,
|
|
342
|
-
}
|
|
343
|
-
: undefined,
|
|
290
|
+
updateLastRoute,
|
|
344
291
|
onRecordError: (err: unknown) => {
|
|
345
292
|
emitBncrLogLine(
|
|
346
293
|
'warn',
|
|
@@ -363,18 +310,13 @@ export async function handleBncrNativeCommand(params: {
|
|
|
363
310
|
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
364
311
|
) => {
|
|
365
312
|
const kind = info?.kind;
|
|
366
|
-
const
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
payload?.text ||
|
|
374
|
-
payload?.mediaUrl ||
|
|
375
|
-
(Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
|
|
376
|
-
);
|
|
377
|
-
if (!hasPayload) return;
|
|
313
|
+
const deliveryPayload = buildBncrNativeReplyDeliveryPayload({
|
|
314
|
+
payload,
|
|
315
|
+
kind,
|
|
316
|
+
effectiveReply,
|
|
317
|
+
msgId,
|
|
318
|
+
});
|
|
319
|
+
if (!deliveryPayload) return;
|
|
378
320
|
if (!responded) {
|
|
379
321
|
logBncrNativeCommandEvent(
|
|
380
322
|
'payload-produced',
|
|
@@ -395,11 +337,8 @@ export async function handleBncrNativeCommand(params: {
|
|
|
395
337
|
accountId,
|
|
396
338
|
sessionKey,
|
|
397
339
|
route,
|
|
398
|
-
payload:
|
|
399
|
-
|
|
400
|
-
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
401
|
-
replyToId: msgId || undefined,
|
|
402
|
-
},
|
|
340
|
+
payload: deliveryPayload,
|
|
341
|
+
replyTargetPolicy: 'preserve',
|
|
403
342
|
});
|
|
404
343
|
},
|
|
405
344
|
},
|
|
@@ -18,15 +18,14 @@ import {
|
|
|
18
18
|
formatOpenClawAgentEnvelope,
|
|
19
19
|
resolveOpenClawEnvelopeFormatOptions,
|
|
20
20
|
} from '../../openclaw/reply-runtime.ts';
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
resolveOpenClawInboundLastRouteSessionKey,
|
|
24
|
-
} from '../../openclaw/routing-runtime.ts';
|
|
21
|
+
import { resolveOpenClawAgentRoute } from '../../openclaw/routing-runtime.ts';
|
|
22
|
+
import type { OutboundReplyTargetPolicy } from '../outbound/reply-target-policy.ts';
|
|
25
23
|
import { handleBncrNativeCommand } from './commands.ts';
|
|
26
24
|
import {
|
|
27
25
|
buildBncrPromptVisibleContextFacts,
|
|
28
26
|
buildBncrStructuredContextFactsFromInboundParts,
|
|
29
27
|
} from './context-facts.ts';
|
|
28
|
+
import { buildBncrInboundRecordUpdateLastRoute } from './last-route.ts';
|
|
30
29
|
import { buildBncrReplyConfig } from './reply-config.ts';
|
|
31
30
|
import { resolveBncrChannelInboundRuntime } from './runtime-compat.ts';
|
|
32
31
|
import { wrapBncrInboundRecordSessionLabelCorrection } from './session-label.ts';
|
|
@@ -330,36 +329,6 @@ function buildBncrInboundTurnContext(args: {
|
|
|
330
329
|
});
|
|
331
330
|
}
|
|
332
331
|
|
|
333
|
-
function buildBncrInboundRecordUpdateLastRoute(args: {
|
|
334
|
-
channelId: string;
|
|
335
|
-
peer: ParsedInbound['peer'];
|
|
336
|
-
senderIdForContext: string;
|
|
337
|
-
resolution: BncrInboundConversationResolution;
|
|
338
|
-
pinnedMainDmOwner: string | null;
|
|
339
|
-
}) {
|
|
340
|
-
const { channelId, peer, senderIdForContext, resolution, pinnedMainDmOwner } = args;
|
|
341
|
-
if (peer.kind !== 'direct') return undefined;
|
|
342
|
-
|
|
343
|
-
const sessionKey = resolveOpenClawInboundLastRouteSessionKey({
|
|
344
|
-
route: resolution.resolvedRoute,
|
|
345
|
-
sessionKey: resolution.dispatchSessionKey,
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
return {
|
|
349
|
-
sessionKey,
|
|
350
|
-
channel: channelId,
|
|
351
|
-
to: resolution.canonicalTo,
|
|
352
|
-
accountId: resolution.accountId,
|
|
353
|
-
mainDmOwnerPin:
|
|
354
|
-
sessionKey === resolution.resolvedRoute.mainSessionKey && pinnedMainDmOwner
|
|
355
|
-
? {
|
|
356
|
-
ownerRecipient: pinnedMainDmOwner,
|
|
357
|
-
senderRecipient: senderIdForContext,
|
|
358
|
-
}
|
|
359
|
-
: undefined,
|
|
360
|
-
};
|
|
361
|
-
}
|
|
362
|
-
|
|
363
332
|
function buildBncrInboundReplyRouteFact(
|
|
364
333
|
resolution: BncrInboundConversationResolution,
|
|
365
334
|
): BncrInboundReplyRouteFact {
|
|
@@ -386,6 +355,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
386
355
|
route: any;
|
|
387
356
|
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
388
357
|
mediaLocalRoots?: readonly string[];
|
|
358
|
+
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
389
359
|
}) => Promise<void>;
|
|
390
360
|
setInboundActivity: (accountId: string, at: number) => void;
|
|
391
361
|
scheduleSave: () => void;
|
|
@@ -478,9 +448,12 @@ export async function dispatchBncrInbound(params: {
|
|
|
478
448
|
: null;
|
|
479
449
|
const updateLastRoute = buildBncrInboundRecordUpdateLastRoute({
|
|
480
450
|
channelId,
|
|
481
|
-
peer,
|
|
451
|
+
peerKind: peer.kind,
|
|
482
452
|
senderIdForContext,
|
|
483
|
-
resolution,
|
|
453
|
+
accountId: resolution.accountId,
|
|
454
|
+
to: resolution.canonicalTo,
|
|
455
|
+
resolvedRoute: resolution.resolvedRoute,
|
|
456
|
+
sessionKey: resolution.dispatchSessionKey,
|
|
484
457
|
pinnedMainDmOwner,
|
|
485
458
|
});
|
|
486
459
|
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { resolveOpenClawInboundLastRouteSessionKey } from '../../openclaw/routing-runtime.ts';
|
|
2
|
+
|
|
3
|
+
export function buildBncrInboundRecordUpdateLastRoute(args: {
|
|
4
|
+
channelId: string;
|
|
5
|
+
peerKind: 'direct' | 'group';
|
|
6
|
+
senderIdForContext: string;
|
|
7
|
+
accountId: string;
|
|
8
|
+
to: string;
|
|
9
|
+
resolvedRoute: {
|
|
10
|
+
sessionKey: string;
|
|
11
|
+
mainSessionKey?: string;
|
|
12
|
+
};
|
|
13
|
+
sessionKey: string;
|
|
14
|
+
pinnedMainDmOwner: string | null;
|
|
15
|
+
}) {
|
|
16
|
+
const {
|
|
17
|
+
channelId,
|
|
18
|
+
peerKind,
|
|
19
|
+
senderIdForContext,
|
|
20
|
+
accountId,
|
|
21
|
+
to,
|
|
22
|
+
resolvedRoute,
|
|
23
|
+
sessionKey,
|
|
24
|
+
pinnedMainDmOwner,
|
|
25
|
+
} = args;
|
|
26
|
+
if (peerKind !== 'direct') return undefined;
|
|
27
|
+
|
|
28
|
+
const inboundLastRouteSessionKey = resolveOpenClawInboundLastRouteSessionKey({
|
|
29
|
+
route: resolvedRoute,
|
|
30
|
+
sessionKey,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
sessionKey: inboundLastRouteSessionKey,
|
|
35
|
+
channel: channelId,
|
|
36
|
+
to,
|
|
37
|
+
accountId,
|
|
38
|
+
mainDmOwnerPin:
|
|
39
|
+
inboundLastRouteSessionKey === resolvedRoute.mainSessionKey && pinnedMainDmOwner
|
|
40
|
+
? {
|
|
41
|
+
ownerRecipient: pinnedMainDmOwner,
|
|
42
|
+
senderRecipient: senderIdForContext,
|
|
43
|
+
}
|
|
44
|
+
: undefined,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export type NativeCommand = {
|
|
2
|
+
command: string;
|
|
3
|
+
raw: string;
|
|
4
|
+
body: string;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export type NativeVerboseCommand = {
|
|
8
|
+
handled: true;
|
|
9
|
+
verboseLevel?: 'on' | 'off' | 'full';
|
|
10
|
+
text: string;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export function parseBncrNativeCommand(text: string): NativeCommand | null {
|
|
14
|
+
const raw = String(text || '').trim();
|
|
15
|
+
if (!raw.startsWith('/')) return null;
|
|
16
|
+
const match = raw.match(/^\/([^\s]+)(?:\s+([\s\S]*))?$/i);
|
|
17
|
+
if (!match) return null;
|
|
18
|
+
|
|
19
|
+
const command = String(match[1] || '')
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase();
|
|
22
|
+
if (!command) return null;
|
|
23
|
+
|
|
24
|
+
const rest = String(match[2] || '').trim();
|
|
25
|
+
const body = command === 'help' ? ['/commands', rest].filter(Boolean).join(' ') : raw;
|
|
26
|
+
return { command, raw, body };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function resolveBncrNativeVerboseCommand(
|
|
30
|
+
command: NativeCommand,
|
|
31
|
+
): NativeVerboseCommand | null {
|
|
32
|
+
if (command.command !== 'verbose') return null;
|
|
33
|
+
const rawLevel = String(command.raw.slice('/verbose'.length) || '')
|
|
34
|
+
.trim()
|
|
35
|
+
.toLowerCase();
|
|
36
|
+
if (!rawLevel || rawLevel === 'status') {
|
|
37
|
+
return { handled: true, text: 'Current verbose level is unchanged.' };
|
|
38
|
+
}
|
|
39
|
+
if (rawLevel === 'on')
|
|
40
|
+
return { handled: true, verboseLevel: 'on', text: 'Verbose logging enabled.' };
|
|
41
|
+
if (rawLevel === 'off')
|
|
42
|
+
return { handled: true, verboseLevel: 'off', text: 'Verbose logging disabled.' };
|
|
43
|
+
if (rawLevel === 'full')
|
|
44
|
+
return { handled: true, verboseLevel: 'full', text: 'Verbose logging set to full.' };
|
|
45
|
+
return {
|
|
46
|
+
handled: true,
|
|
47
|
+
text: `Unrecognized verbose level "${rawLevel}". Valid levels: off, on, full.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
type BncrNativeReplyKind = 'tool' | 'block' | 'final';
|
|
2
|
+
|
|
3
|
+
export type BncrNativeReplyDeliveryPayload = {
|
|
4
|
+
text?: string;
|
|
5
|
+
mediaUrl?: string;
|
|
6
|
+
mediaUrls?: string[];
|
|
7
|
+
audioAsVoice?: boolean;
|
|
8
|
+
replyToId?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function buildBncrNativeReplyDeliveryPayload(args: {
|
|
12
|
+
payload?: {
|
|
13
|
+
text?: string;
|
|
14
|
+
mediaUrl?: string;
|
|
15
|
+
mediaUrls?: string[];
|
|
16
|
+
audioAsVoice?: boolean;
|
|
17
|
+
};
|
|
18
|
+
kind?: BncrNativeReplyKind;
|
|
19
|
+
effectiveReply: {
|
|
20
|
+
blockStreaming: boolean;
|
|
21
|
+
allowTool: boolean;
|
|
22
|
+
};
|
|
23
|
+
msgId?: string;
|
|
24
|
+
}): BncrNativeReplyDeliveryPayload | null {
|
|
25
|
+
const { payload, kind, effectiveReply, msgId } = args;
|
|
26
|
+
const shouldForwardTool = effectiveReply.blockStreaming && effectiveReply.allowTool;
|
|
27
|
+
|
|
28
|
+
if (kind === 'tool' && !shouldForwardTool) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const hasPayload = Boolean(
|
|
33
|
+
payload?.text ||
|
|
34
|
+
payload?.mediaUrl ||
|
|
35
|
+
(Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0),
|
|
36
|
+
);
|
|
37
|
+
if (!hasPayload) return null;
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
...payload,
|
|
41
|
+
replyToId: msgId || undefined,
|
|
42
|
+
};
|
|
43
|
+
}
|