@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.
@@ -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
- '[bncr] using legacy runtime.channel.turn compatibility path; upgrade path prefers runtime.channel.inbound',
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
- type OutboundScheduleSource,
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,8 @@
1
+ import type { ChatType } from 'openclaw/plugin-sdk';
2
+
3
+ export const BNCR_CHANNEL_CAPABILITIES = {
4
+ chatTypes: ['direct'] as ChatType[],
5
+ media: true,
6
+ reply: true,
7
+ nativeCommands: true,
8
+ };
@@ -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,12 @@
1
+ export const BNCR_GATEWAY_METHODS = [
2
+ 'bncr.connect',
3
+ 'bncr.inbound',
4
+ 'bncr.activity',
5
+ 'bncr.ack',
6
+ 'bncr.diagnostics',
7
+ 'bncr.file.init',
8
+ 'bncr.file.chunk',
9
+ 'bncr.file.complete',
10
+ 'bncr.file.abort',
11
+ 'bncr.file.ack',
12
+ ];
@@ -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,4 @@
1
+ export const BNCR_MESSAGE_RECEIVE_POLICY = {
2
+ defaultAckPolicy: 'manual' as const,
3
+ supportedAckPolicies: ['manual'] as const,
4
+ };
@@ -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,10 @@
1
+ import { CHANNEL_ID } from '../core/accounts.ts';
2
+
3
+ export const BNCR_CHANNEL_META = {
4
+ id: CHANNEL_ID,
5
+ label: 'Bncr',
6
+ selectionLabel: 'Bncr Client',
7
+ docsPath: '/channels/bncr',
8
+ blurb: 'Bncr Channel.',
9
+ aliases: ['bncr'],
10
+ };
@@ -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
+ }