@xmoxmo/bncr 0.2.7 → 0.2.9
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/package.json +1 -1
- package/scripts/check-pack.mjs +21 -5
- package/src/channel.ts +668 -766
- package/src/core/extended-diagnostics.ts +10 -0
- package/src/core/file-ack.ts +9 -0
- package/src/core/file-transfer-payloads.ts +72 -0
- package/src/core/register-trace.ts +79 -0
- package/src/messaging/inbound/runtime-compat.ts +3 -1
- package/src/messaging/outbound/diagnostics.ts +128 -6
- package/src/plugin/capabilities.ts +8 -0
- package/src/plugin/config.ts +35 -0
- package/src/plugin/gateway-methods.ts +12 -0
- package/src/plugin/gateway-runtime.ts +11 -0
- package/src/plugin/message-policy.ts +4 -0
- package/src/plugin/message-send.ts +13 -0
- package/src/plugin/messaging.ts +142 -0
- package/src/plugin/meta.ts +10 -0
- package/src/plugin/outbound.ts +51 -0
- package/src/plugin/setup.ts +24 -0
- package/src/plugin/status.ts +38 -0
- package/src/runtime/log-dedupe.ts +56 -0
- package/src/runtime/outbound-ack-timeout.ts +96 -0
- package/src/runtime/outbound-flags.ts +81 -0
- package/src/runtime/outbox-transitions.ts +119 -0
- package/src/runtime/status-snapshots.ts +108 -0
- package/src/runtime/status-worker.ts +172 -0
|
@@ -2,6 +2,10 @@ import type { RegisterTraceEntry } from './register-trace.ts';
|
|
|
2
2
|
|
|
3
3
|
type ExtendedDiagnosticsInput = {
|
|
4
4
|
diagnostics: Record<string, any>;
|
|
5
|
+
runtimeSurface?: {
|
|
6
|
+
channel: Record<string, boolean>;
|
|
7
|
+
missing: string[];
|
|
8
|
+
};
|
|
5
9
|
register: {
|
|
6
10
|
bridgeId: string;
|
|
7
11
|
gatewayPid: number;
|
|
@@ -48,6 +52,12 @@ type ExtendedDiagnosticsInput = {
|
|
|
48
52
|
export function buildExtendedDiagnostics(input: ExtendedDiagnosticsInput) {
|
|
49
53
|
return {
|
|
50
54
|
...input.diagnostics,
|
|
55
|
+
runtimeSurface: input.runtimeSurface
|
|
56
|
+
? {
|
|
57
|
+
channel: { ...input.runtimeSurface.channel },
|
|
58
|
+
missing: input.runtimeSurface.missing.slice(),
|
|
59
|
+
}
|
|
60
|
+
: undefined,
|
|
51
61
|
register: {
|
|
52
62
|
...input.register,
|
|
53
63
|
traceRecent: input.register.traceRecent.slice(),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function buildFileAckKey(args: {
|
|
2
|
+
transferId: string;
|
|
3
|
+
stage: string;
|
|
4
|
+
chunkIndex?: number;
|
|
5
|
+
}): string {
|
|
6
|
+
const n = Number(args.chunkIndex);
|
|
7
|
+
const idx = Number.isInteger(n) && n >= 0 ? String(n) : '-';
|
|
8
|
+
return `${args.transferId}|${args.stage}|${idx}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { BncrRoute } from './accounts.ts';
|
|
2
|
+
|
|
3
|
+
export function buildFileTransferInitPayload(args: {
|
|
4
|
+
transferId: string;
|
|
5
|
+
sessionKey: string;
|
|
6
|
+
route: BncrRoute;
|
|
7
|
+
fileName: string;
|
|
8
|
+
mimeType?: string;
|
|
9
|
+
fileSize: number;
|
|
10
|
+
chunkSize: number;
|
|
11
|
+
totalChunks: number;
|
|
12
|
+
fileSha256: string;
|
|
13
|
+
ts: number;
|
|
14
|
+
}) {
|
|
15
|
+
return {
|
|
16
|
+
transferId: args.transferId,
|
|
17
|
+
direction: 'oc2bncr' as const,
|
|
18
|
+
sessionKey: args.sessionKey,
|
|
19
|
+
platform: args.route.platform,
|
|
20
|
+
groupId: args.route.groupId,
|
|
21
|
+
userId: args.route.userId,
|
|
22
|
+
fileName: args.fileName,
|
|
23
|
+
mimeType: args.mimeType,
|
|
24
|
+
fileSize: args.fileSize,
|
|
25
|
+
chunkSize: args.chunkSize,
|
|
26
|
+
totalChunks: args.totalChunks,
|
|
27
|
+
fileSha256: args.fileSha256,
|
|
28
|
+
ts: args.ts,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildFileTransferChunkPayload(args: {
|
|
33
|
+
transferId: string;
|
|
34
|
+
chunkIndex: number;
|
|
35
|
+
offset: number;
|
|
36
|
+
size: number;
|
|
37
|
+
chunkSha256: string;
|
|
38
|
+
base64: string;
|
|
39
|
+
ts: number;
|
|
40
|
+
}) {
|
|
41
|
+
return {
|
|
42
|
+
transferId: args.transferId,
|
|
43
|
+
chunkIndex: args.chunkIndex,
|
|
44
|
+
offset: args.offset,
|
|
45
|
+
size: args.size,
|
|
46
|
+
chunkSha256: args.chunkSha256,
|
|
47
|
+
base64: args.base64,
|
|
48
|
+
ts: args.ts,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildFileTransferAbortPayload(args: {
|
|
53
|
+
transferId: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
ts: number;
|
|
56
|
+
}) {
|
|
57
|
+
return {
|
|
58
|
+
transferId: args.transferId,
|
|
59
|
+
reason: args.reason,
|
|
60
|
+
ts: args.ts,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildFileTransferCompletePayload(args: {
|
|
65
|
+
transferId: string;
|
|
66
|
+
ts: number;
|
|
67
|
+
}) {
|
|
68
|
+
return {
|
|
69
|
+
transferId: args.transferId,
|
|
70
|
+
ts: args.ts,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -33,6 +33,19 @@ export type RegisterTraceSummary = {
|
|
|
33
33
|
likelyStartupFanoutOnly: boolean;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
export type RegisterDriftSnapshot = {
|
|
37
|
+
capturedAt: number;
|
|
38
|
+
registerCount: number;
|
|
39
|
+
apiGeneration: number;
|
|
40
|
+
postWarmupRegisterCount: number;
|
|
41
|
+
apiInstanceId: string | null;
|
|
42
|
+
registryFingerprint: string | null;
|
|
43
|
+
dominantBucket: string | null;
|
|
44
|
+
sourceBuckets: Record<string, number>;
|
|
45
|
+
traceWindowSize: number;
|
|
46
|
+
traceRecent: Array<Record<string, unknown>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
36
49
|
export function classifyRegisterTrace(stack: string) {
|
|
37
50
|
if (
|
|
38
51
|
stack.includes('prepareSecretsRuntimeSnapshot') ||
|
|
@@ -68,6 +81,72 @@ export function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
|
|
|
68
81
|
return winner;
|
|
69
82
|
}
|
|
70
83
|
|
|
84
|
+
export function buildRegisterTraceEntry(args: {
|
|
85
|
+
ts: number;
|
|
86
|
+
bridgeId: string;
|
|
87
|
+
gatewayPid: number;
|
|
88
|
+
registerCount: number;
|
|
89
|
+
apiGeneration: number;
|
|
90
|
+
apiRebound: boolean;
|
|
91
|
+
apiInstanceId: string | null;
|
|
92
|
+
registryFingerprint: string | null;
|
|
93
|
+
source: string | null;
|
|
94
|
+
pluginVersion: string | null;
|
|
95
|
+
stack: string;
|
|
96
|
+
}): RegisterTraceEntry {
|
|
97
|
+
return {
|
|
98
|
+
ts: args.ts,
|
|
99
|
+
bridgeId: args.bridgeId,
|
|
100
|
+
gatewayPid: args.gatewayPid,
|
|
101
|
+
registerCount: args.registerCount,
|
|
102
|
+
apiGeneration: args.apiGeneration,
|
|
103
|
+
apiRebound: args.apiRebound,
|
|
104
|
+
apiInstanceId: args.apiInstanceId,
|
|
105
|
+
registryFingerprint: args.registryFingerprint,
|
|
106
|
+
source: args.source,
|
|
107
|
+
pluginVersion: args.pluginVersion,
|
|
108
|
+
stack: args.stack,
|
|
109
|
+
stackBucket: classifyRegisterTrace(args.stack),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function appendBoundedRegisterTrace(
|
|
114
|
+
traceRecent: RegisterTraceEntry[],
|
|
115
|
+
trace: RegisterTraceEntry,
|
|
116
|
+
maxEntries = 12,
|
|
117
|
+
) {
|
|
118
|
+
traceRecent.push(trace);
|
|
119
|
+
const cap = Math.max(0, Math.floor(finiteNumberOr(maxEntries, 12)));
|
|
120
|
+
if (cap === 0) {
|
|
121
|
+
traceRecent.splice(0, traceRecent.length);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (traceRecent.length > cap) traceRecent.splice(0, traceRecent.length - cap);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function buildRegisterDriftSnapshot(args: {
|
|
128
|
+
capturedAt: number;
|
|
129
|
+
registerCount: number;
|
|
130
|
+
apiGeneration: number;
|
|
131
|
+
summary: RegisterTraceSummary;
|
|
132
|
+
apiInstanceId: string | null;
|
|
133
|
+
registryFingerprint: string | null;
|
|
134
|
+
traceRecent: RegisterTraceEntry[];
|
|
135
|
+
}): RegisterDriftSnapshot {
|
|
136
|
+
return {
|
|
137
|
+
capturedAt: args.capturedAt,
|
|
138
|
+
registerCount: args.registerCount,
|
|
139
|
+
apiGeneration: args.apiGeneration,
|
|
140
|
+
postWarmupRegisterCount: args.summary.postWarmupRegisterCount,
|
|
141
|
+
apiInstanceId: args.apiInstanceId,
|
|
142
|
+
registryFingerprint: args.registryFingerprint,
|
|
143
|
+
dominantBucket: args.summary.dominantBucket,
|
|
144
|
+
sourceBuckets: { ...args.summary.sourceBuckets },
|
|
145
|
+
traceWindowSize: args.traceRecent.length,
|
|
146
|
+
traceRecent: args.traceRecent.map((trace) => ({ ...trace })),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
71
150
|
export function buildRegisterTraceSummary(args: {
|
|
72
151
|
traceRecent: RegisterTraceEntry[];
|
|
73
152
|
firstRegisterAt: number | null;
|
|
@@ -20,9 +20,11 @@ export function resolveBncrChannelInboundRuntime(api: any): ChannelRuntimeCompat
|
|
|
20
20
|
if (legacyTurnRuntime?.buildContext && legacyTurnRuntime?.run) {
|
|
21
21
|
if (!warnedLegacyTurnRuntime) {
|
|
22
22
|
warnedLegacyTurnRuntime = true;
|
|
23
|
+
const channelRuntimeKeys = Object.keys(channelRuntime ?? {}).sort().join(',') || 'none';
|
|
24
|
+
const inboundRuntimeKeys = Object.keys(inboundRuntime ?? {}).sort().join(',') || 'none';
|
|
23
25
|
emitBncrLogLine(
|
|
24
26
|
'warn',
|
|
25
|
-
|
|
27
|
+
`[bncr] inbound runtime fallback=turn|preferred=inbound|channelKeys=${channelRuntimeKeys}|inboundKeys=${inboundRuntimeKeys}`,
|
|
26
28
|
);
|
|
27
29
|
}
|
|
28
30
|
return {
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
OUTBOUND_TERMINAL_REASON,
|
|
4
|
-
} from './reasons.ts';
|
|
1
|
+
import type { BncrConnection } from '../../core/types.ts';
|
|
2
|
+
import { OUTBOUND_TERMINAL_REASON, type OutboundScheduleSource } from './reasons.ts';
|
|
5
3
|
import type { RetryRerouteDecision } from './retry-policy.ts';
|
|
6
4
|
|
|
7
5
|
export function buildOutboxScheduleDebugInfo(args: {
|
|
@@ -33,12 +31,43 @@ export function buildOutboxPushSkipDebugInfo(args: {
|
|
|
33
31
|
reason: string;
|
|
34
32
|
recentInboundReachable?: boolean;
|
|
35
33
|
kind?: string;
|
|
34
|
+
routeReason?: string;
|
|
35
|
+
connIds?: Iterable<string>;
|
|
36
|
+
ownerConnId?: string;
|
|
37
|
+
ownerClientId?: string;
|
|
38
|
+
activeConnectionCount?: number;
|
|
39
|
+
connections?: Iterable<BncrConnection>;
|
|
36
40
|
}) {
|
|
37
41
|
return {
|
|
38
42
|
messageId: args.messageId,
|
|
39
43
|
accountId: args.accountId,
|
|
40
44
|
...(args.kind ? { kind: args.kind } : {}),
|
|
41
45
|
reason: args.reason,
|
|
46
|
+
...(args.routeReason ? { routeReason: args.routeReason } : {}),
|
|
47
|
+
...(args.connIds ? { connIds: Array.from(args.connIds) } : {}),
|
|
48
|
+
...(args.ownerConnId ? { ownerConnId: args.ownerConnId } : {}),
|
|
49
|
+
...(args.ownerClientId ? { ownerClientId: args.ownerClientId } : {}),
|
|
50
|
+
...(typeof args.activeConnectionCount === 'number'
|
|
51
|
+
? { activeConnectionCount: args.activeConnectionCount }
|
|
52
|
+
: {}),
|
|
53
|
+
...(args.connections
|
|
54
|
+
? {
|
|
55
|
+
connections: Array.from(args.connections)
|
|
56
|
+
.filter((c) => c.accountId === args.accountId)
|
|
57
|
+
.slice(0, 8)
|
|
58
|
+
.map((c) => ({
|
|
59
|
+
connId: c.connId,
|
|
60
|
+
clientId: c.clientId,
|
|
61
|
+
lastSeenAt: c.lastSeenAt,
|
|
62
|
+
outboundReadyUntil: (c as any).outboundReadyUntil,
|
|
63
|
+
preferredForOutboundUntil: (c as any).preferredForOutboundUntil,
|
|
64
|
+
inboundOnly: (c as any).inboundOnly,
|
|
65
|
+
lastAckOkAt: (c as any).lastAckOkAt,
|
|
66
|
+
lastPushTimeoutAt: (c as any).lastPushTimeoutAt,
|
|
67
|
+
pushFailureScore: (c as any).pushFailureScore,
|
|
68
|
+
})),
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
42
71
|
...(typeof args.recentInboundReachable === 'boolean'
|
|
43
72
|
? { recentInboundReachable: args.recentInboundReachable }
|
|
44
73
|
: {}),
|
|
@@ -109,6 +138,100 @@ export function buildFlushDebugInfo(args: {
|
|
|
109
138
|
};
|
|
110
139
|
}
|
|
111
140
|
|
|
141
|
+
export function buildOutboxDrainSkipDebugInfo(args: {
|
|
142
|
+
bridgeId: string;
|
|
143
|
+
accountId: string;
|
|
144
|
+
reason: string;
|
|
145
|
+
outboxSize: number;
|
|
146
|
+
trigger: string;
|
|
147
|
+
}) {
|
|
148
|
+
return {
|
|
149
|
+
bridge: args.bridgeId,
|
|
150
|
+
accountId: args.accountId,
|
|
151
|
+
reason: args.reason,
|
|
152
|
+
outboxSize: args.outboxSize,
|
|
153
|
+
trigger: args.trigger,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function buildOutboxDrainStuckDebugInfo(args: {
|
|
158
|
+
bridgeId: string;
|
|
159
|
+
accountId: string;
|
|
160
|
+
reason: string;
|
|
161
|
+
trigger: string;
|
|
162
|
+
outboxSize: number;
|
|
163
|
+
pending: number;
|
|
164
|
+
runningMs: number;
|
|
165
|
+
runningSince?: number | null;
|
|
166
|
+
hasGatewayContext: boolean;
|
|
167
|
+
activeConnectionCount: number;
|
|
168
|
+
messageAckWaiters: number;
|
|
169
|
+
fileAckWaiters: number;
|
|
170
|
+
pendingEntries?: Iterable<{
|
|
171
|
+
messageId?: string;
|
|
172
|
+
retryCount?: number;
|
|
173
|
+
nextAttemptAt?: number;
|
|
174
|
+
lastAttemptAt?: number;
|
|
175
|
+
lastError?: string;
|
|
176
|
+
lastPushAt?: number;
|
|
177
|
+
lastPushConnId?: string;
|
|
178
|
+
routeAttemptConnIds?: string[];
|
|
179
|
+
}>;
|
|
180
|
+
connections?: Iterable<BncrConnection>;
|
|
181
|
+
}) {
|
|
182
|
+
return {
|
|
183
|
+
bridge: args.bridgeId,
|
|
184
|
+
accountId: args.accountId,
|
|
185
|
+
reason: args.reason,
|
|
186
|
+
trigger: args.trigger,
|
|
187
|
+
outboxSize: args.outboxSize,
|
|
188
|
+
pending: args.pending,
|
|
189
|
+
runningMs: args.runningMs,
|
|
190
|
+
runningSince: args.runningSince || null,
|
|
191
|
+
hasGatewayContext: args.hasGatewayContext,
|
|
192
|
+
activeConnectionCount: args.activeConnectionCount,
|
|
193
|
+
waiters: {
|
|
194
|
+
messageAck: args.messageAckWaiters,
|
|
195
|
+
fileAck: args.fileAckWaiters,
|
|
196
|
+
},
|
|
197
|
+
...(args.pendingEntries
|
|
198
|
+
? {
|
|
199
|
+
pendingEntries: Array.from(args.pendingEntries)
|
|
200
|
+
.slice(0, 8)
|
|
201
|
+
.map((entry) => ({
|
|
202
|
+
messageId: entry.messageId || '',
|
|
203
|
+
retryCount: entry.retryCount,
|
|
204
|
+
nextAttemptAt: entry.nextAttemptAt,
|
|
205
|
+
lastAttemptAt: entry.lastAttemptAt,
|
|
206
|
+
lastError: entry.lastError,
|
|
207
|
+
lastPushAt: entry.lastPushAt,
|
|
208
|
+
lastPushConnId: entry.lastPushConnId,
|
|
209
|
+
routeAttemptConnIds: entry.routeAttemptConnIds,
|
|
210
|
+
})),
|
|
211
|
+
}
|
|
212
|
+
: {}),
|
|
213
|
+
...(args.connections
|
|
214
|
+
? {
|
|
215
|
+
connections: Array.from(args.connections)
|
|
216
|
+
.filter((c) => c.accountId === args.accountId)
|
|
217
|
+
.slice(0, 8)
|
|
218
|
+
.map((c) => ({
|
|
219
|
+
connId: c.connId,
|
|
220
|
+
clientId: c.clientId,
|
|
221
|
+
connectedAt: c.connectedAt,
|
|
222
|
+
lastSeenAt: c.lastSeenAt,
|
|
223
|
+
outboundReadyUntil: (c as any).outboundReadyUntil,
|
|
224
|
+
preferredForOutboundUntil: (c as any).preferredForOutboundUntil,
|
|
225
|
+
inboundOnly: (c as any).inboundOnly,
|
|
226
|
+
lastAckOkAt: (c as any).lastAckOkAt,
|
|
227
|
+
lastPushTimeoutAt: (c as any).lastPushTimeoutAt,
|
|
228
|
+
pushFailureScore: (c as any).pushFailureScore,
|
|
229
|
+
})),
|
|
230
|
+
}
|
|
231
|
+
: {}),
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
112
235
|
export function buildOutboxAckDebugInfo(args: {
|
|
113
236
|
messageId: string;
|
|
114
237
|
accountId: string;
|
|
@@ -207,8 +330,7 @@ export function buildPushFailureDebugInfo(args: {
|
|
|
207
330
|
...(typeof args.retryable === 'boolean' ? { retryable: args.retryable } : {}),
|
|
208
331
|
retryCount: args.retryCount,
|
|
209
332
|
error:
|
|
210
|
-
(typeof args.lastError === 'string' && args.lastError) ||
|
|
211
|
-
OUTBOUND_TERMINAL_REASON.PUSH_RETRY,
|
|
333
|
+
(typeof args.lastError === 'string' && args.lastError) || OUTBOUND_TERMINAL_REASON.PUSH_RETRY,
|
|
212
334
|
};
|
|
213
335
|
}
|
|
214
336
|
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHANNEL_ID,
|
|
3
|
+
listAccountIds,
|
|
4
|
+
resolveAccount,
|
|
5
|
+
resolveDefaultDisplayName,
|
|
6
|
+
} from '../core/accounts.ts';
|
|
7
|
+
import { resolveBncrChannelPolicy } from '../core/policy.ts';
|
|
8
|
+
import { setOpenClawAccountEnabledInConfigSection } from '../openclaw/sdk-helpers.ts';
|
|
9
|
+
|
|
10
|
+
export const BNCR_CONFIG_SURFACE = {
|
|
11
|
+
listAccountIds,
|
|
12
|
+
resolveAccount,
|
|
13
|
+
setAccountEnabled: ({ cfg, accountId, enabled }: any) =>
|
|
14
|
+
setOpenClawAccountEnabledInConfigSection({
|
|
15
|
+
cfg,
|
|
16
|
+
sectionKey: CHANNEL_ID,
|
|
17
|
+
accountId,
|
|
18
|
+
enabled,
|
|
19
|
+
allowTopLevel: true,
|
|
20
|
+
}),
|
|
21
|
+
isEnabled: (account: any, cfg: any) => {
|
|
22
|
+
const policy = resolveBncrChannelPolicy(cfg?.channels?.[CHANNEL_ID] || {});
|
|
23
|
+
return policy.enabled !== false && account?.enabled !== false;
|
|
24
|
+
},
|
|
25
|
+
isConfigured: () => true,
|
|
26
|
+
describeAccount: (account: any) => {
|
|
27
|
+
const displayName = resolveDefaultDisplayName(account?.name, account?.accountId);
|
|
28
|
+
return {
|
|
29
|
+
accountId: account.accountId,
|
|
30
|
+
name: displayName,
|
|
31
|
+
enabled: account.enabled !== false,
|
|
32
|
+
configured: true,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type BncrGatewayAccountBridge = {
|
|
2
|
+
channelStartAccount: (ctx: any) => unknown | Promise<unknown>;
|
|
3
|
+
channelStopAccount: (ctx: any) => unknown | Promise<unknown>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function createBncrGatewayRuntime(getBridge: () => BncrGatewayAccountBridge) {
|
|
7
|
+
return {
|
|
8
|
+
startAccount: async (ctx: any) => getBridge().channelStartAccount(ctx),
|
|
9
|
+
stopAccount: async (ctx: any) => getBridge().channelStopAccount(ctx),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type BncrMessageSendBridge = {
|
|
2
|
+
channelMessageSendText: (ctx: any) => unknown | Promise<unknown>;
|
|
3
|
+
channelMessageSendMedia: (ctx: any) => unknown | Promise<unknown>;
|
|
4
|
+
channelMessageSendPayload: (ctx: any) => unknown | Promise<unknown>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function createBncrMessageSend(getBridge: () => BncrMessageSendBridge) {
|
|
8
|
+
return {
|
|
9
|
+
text: async (ctx: any) => getBridge().channelMessageSendText(ctx),
|
|
10
|
+
media: async (ctx: any) => getBridge().channelMessageSendMedia(ctx),
|
|
11
|
+
payload: async (ctx: any) => getBridge().channelMessageSendPayload(ctx),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { BNCR_DEFAULT_ACCOUNT_ID, normalizeAccountId } from '../core/accounts.ts';
|
|
2
|
+
import {
|
|
3
|
+
formatDisplayScope,
|
|
4
|
+
formatTargetDisplay,
|
|
5
|
+
parseExplicitTarget,
|
|
6
|
+
} from '../core/targets.ts';
|
|
7
|
+
import { resolveBncrOutboundSessionRoute } from '../messaging/outbound/session-route.ts';
|
|
8
|
+
import {
|
|
9
|
+
looksLikeBncrExplicitTarget,
|
|
10
|
+
resolveBncrOutboundTarget,
|
|
11
|
+
} from '../messaging/outbound/target-resolver.ts';
|
|
12
|
+
|
|
13
|
+
type BncrMessagingRuntimeBridge = {
|
|
14
|
+
canonicalAgentId?: string;
|
|
15
|
+
ensureCanonicalAgentId: (params: { cfg: any; accountId: string }) => string;
|
|
16
|
+
resolveRouteBySession: (raw: string, accountId: string) => any;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function asString(v: unknown, fallback = ''): string {
|
|
20
|
+
return typeof v === 'string' ? v : fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeBncrMessagingTarget(raw: string) {
|
|
24
|
+
const input = asString(raw).trim();
|
|
25
|
+
return input || undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatBncrMessagingTargetDisplay({ target }: any) {
|
|
29
|
+
return formatTargetDisplay(target);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveMessagingAccountId(accountId: unknown) {
|
|
33
|
+
return normalizeAccountId(asString(accountId || BNCR_DEFAULT_ACCOUNT_ID));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveMessagingCanonicalAgentId(
|
|
37
|
+
runtimeBridge: BncrMessagingRuntimeBridge,
|
|
38
|
+
cfg: any,
|
|
39
|
+
accountId: string,
|
|
40
|
+
) {
|
|
41
|
+
return (
|
|
42
|
+
runtimeBridge.canonicalAgentId ||
|
|
43
|
+
runtimeBridge.ensureCanonicalAgentId({ cfg, accountId })
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createBncrMessagingExplicitTargetParser(
|
|
48
|
+
getBridge: () => BncrMessagingRuntimeBridge,
|
|
49
|
+
) {
|
|
50
|
+
return ({ raw, accountId, cfg }: any) => {
|
|
51
|
+
const resolvedAccountId = resolveMessagingAccountId(accountId);
|
|
52
|
+
const runtimeBridge = getBridge();
|
|
53
|
+
const canonicalAgentId = resolveMessagingCanonicalAgentId(
|
|
54
|
+
runtimeBridge,
|
|
55
|
+
cfg,
|
|
56
|
+
resolvedAccountId,
|
|
57
|
+
);
|
|
58
|
+
return parseExplicitTarget(asString(raw).trim(), { canonicalAgentId });
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createBncrMessagingSessionTargetResolver(
|
|
63
|
+
getBridge: () => BncrMessagingRuntimeBridge,
|
|
64
|
+
) {
|
|
65
|
+
return ({ id, accountId, cfg }: any) => {
|
|
66
|
+
const raw = asString(id).trim();
|
|
67
|
+
if (!raw) return undefined;
|
|
68
|
+
const resolvedAccountId = resolveMessagingAccountId(accountId);
|
|
69
|
+
const runtimeBridge = getBridge();
|
|
70
|
+
const canonicalAgentId = resolveMessagingCanonicalAgentId(
|
|
71
|
+
runtimeBridge,
|
|
72
|
+
cfg,
|
|
73
|
+
resolvedAccountId,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
let parsed = parseExplicitTarget(raw, { canonicalAgentId });
|
|
77
|
+
if (!parsed) {
|
|
78
|
+
const route = runtimeBridge.resolveRouteBySession(raw, resolvedAccountId);
|
|
79
|
+
if (route) {
|
|
80
|
+
parsed = parseExplicitTarget(formatDisplayScope(route), { canonicalAgentId });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return parsed?.displayScope || undefined;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createBncrMessagingOutboundSessionRouteResolver(
|
|
88
|
+
getBridge: () => BncrMessagingRuntimeBridge,
|
|
89
|
+
) {
|
|
90
|
+
return (params: any) => {
|
|
91
|
+
const accountId = resolveMessagingAccountId(params?.accountId);
|
|
92
|
+
const runtimeBridge = getBridge();
|
|
93
|
+
const canonicalAgentId = resolveMessagingCanonicalAgentId(
|
|
94
|
+
runtimeBridge,
|
|
95
|
+
params?.cfg,
|
|
96
|
+
accountId,
|
|
97
|
+
);
|
|
98
|
+
return resolveBncrOutboundSessionRoute({
|
|
99
|
+
...params,
|
|
100
|
+
canonicalAgentId,
|
|
101
|
+
resolveRouteBySession: (raw: string, acc: string) =>
|
|
102
|
+
runtimeBridge.resolveRouteBySession(raw, acc),
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createBncrMessagingSurface(getBridge: () => BncrMessagingRuntimeBridge) {
|
|
108
|
+
return {
|
|
109
|
+
// 接收任意标签输入;不在 normalize 阶段做格式门槛,统一下沉到发送前验证。
|
|
110
|
+
normalizeTarget: normalizeBncrMessagingTarget,
|
|
111
|
+
parseExplicitTarget: createBncrMessagingExplicitTargetParser(getBridge),
|
|
112
|
+
formatTargetDisplay: formatBncrMessagingTargetDisplay,
|
|
113
|
+
resolveSessionTarget: createBncrMessagingSessionTargetResolver(getBridge),
|
|
114
|
+
resolveOutboundSessionRoute: createBncrMessagingOutboundSessionRouteResolver(getBridge),
|
|
115
|
+
targetResolver: createBncrMessagingTargetResolver(getBridge),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createBncrMessagingTargetResolver(getBridge: () => BncrMessagingRuntimeBridge) {
|
|
120
|
+
return {
|
|
121
|
+
looksLikeId: (raw: string, normalized?: string) => {
|
|
122
|
+
return looksLikeBncrExplicitTarget(asString(normalized || raw).trim());
|
|
123
|
+
},
|
|
124
|
+
resolveTarget: async ({ accountId, input, normalized }: any) => {
|
|
125
|
+
const runtimeBridge = getBridge();
|
|
126
|
+
const resolved = resolveBncrOutboundTarget({
|
|
127
|
+
target: asString(normalized || input).trim(),
|
|
128
|
+
accountId: resolveMessagingAccountId(accountId),
|
|
129
|
+
resolveRouteBySession: (raw: string, acc: string) =>
|
|
130
|
+
runtimeBridge.resolveRouteBySession(raw, acc),
|
|
131
|
+
});
|
|
132
|
+
if (!resolved) return null;
|
|
133
|
+
return {
|
|
134
|
+
to: resolved.displayScope,
|
|
135
|
+
kind: resolved.kind,
|
|
136
|
+
display: resolved.displayScope,
|
|
137
|
+
source: 'normalized' as const,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent:<agentId>:bncr:direct:<hex>',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { normalizeAccountId } from '../core/accounts.ts';
|
|
2
|
+
import {
|
|
3
|
+
deleteBncrMessageAction,
|
|
4
|
+
editBncrMessageAction,
|
|
5
|
+
reactBncrMessageAction,
|
|
6
|
+
sendBncrReplyAction,
|
|
7
|
+
} from '../messaging/outbound/actions.ts';
|
|
8
|
+
|
|
9
|
+
function asString(v: unknown, fallback = ''): string {
|
|
10
|
+
return typeof v === 'string' ? v : v == null ? fallback : String(v);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type BncrOutboundBridge = {
|
|
14
|
+
channelSendText: (ctx: any) => unknown | Promise<unknown>;
|
|
15
|
+
channelSendMedia: (ctx: any) => unknown | Promise<unknown>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function createBncrOutboundRuntime(getBridge: () => BncrOutboundBridge) {
|
|
19
|
+
return {
|
|
20
|
+
deliveryMode: 'gateway' as const,
|
|
21
|
+
sendText: async (ctx: any) => getBridge().channelSendText(ctx),
|
|
22
|
+
sendMedia: async (ctx: any) => getBridge().channelSendMedia(ctx),
|
|
23
|
+
replyAction: async (ctx: any) =>
|
|
24
|
+
sendBncrReplyAction({
|
|
25
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
26
|
+
to: asString(ctx?.to || '').trim(),
|
|
27
|
+
text: asString(ctx?.text || ''),
|
|
28
|
+
replyToMessageId:
|
|
29
|
+
asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
30
|
+
sendText: async ({ accountId, to, text }) =>
|
|
31
|
+
getBridge().channelSendText({ accountId, to, text }),
|
|
32
|
+
}),
|
|
33
|
+
deleteAction: async (ctx: any) =>
|
|
34
|
+
deleteBncrMessageAction({
|
|
35
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
36
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
37
|
+
}),
|
|
38
|
+
reactAction: async (ctx: any) =>
|
|
39
|
+
reactBncrMessageAction({
|
|
40
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
41
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
42
|
+
emoji: asString(ctx?.emoji || '').trim(),
|
|
43
|
+
}),
|
|
44
|
+
editAction: async (ctx: any) =>
|
|
45
|
+
editBncrMessageAction({
|
|
46
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
47
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
48
|
+
text: asString(ctx?.text || ''),
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|