@xmoxmo/bncr 0.6.7 → 0.6.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/src/channel.ts CHANGED
@@ -201,6 +201,7 @@ import {
201
201
  createBncrOutboxDiagnosticsHelpers,
202
202
  createBncrRuntimeAckObservabilityBuilder,
203
203
  } from './plugin/runtime-diagnostics-snapshot.ts';
204
+ import { buildRouteSceneKey } from './plugin/scene-registry.ts';
204
205
  import { BNCR_SETUP_SURFACE } from './plugin/setup.ts';
205
206
  import type { BncrHistoryShardRow } from './plugin/sqlite-state.ts';
206
207
  import { createBncrStateTransientRuntimeGroup } from './plugin/state-transient-runtime-group.ts';
@@ -309,6 +310,12 @@ class BncrBridgeRuntime {
309
310
  getSceneRegistry(): Map<string, BncrSceneRecord> {
310
311
  return this.sceneRegistry;
311
312
  }
313
+ resolveOutboundAgentId(route: BncrRoute): string {
314
+ return (
315
+ asString(this.sceneRegistry.get(buildRouteSceneKey(route))?.agentId).trim() ||
316
+ this.defaultPublicAgentId()
317
+ );
318
+ }
312
319
  private conversationHistories: BncrConversationHistoryMap = new Map();
313
320
  private outboundReplayCache: BncrOutboundReplayCache = new Map();
314
321
  private routeAliases = new Map<
@@ -348,6 +355,7 @@ class BncrBridgeRuntime {
348
355
  // 内置健康/回归计数(替代独立脚本)
349
356
  private startedAt = now();
350
357
  private stopped = false;
358
+ private serviceStarted = false;
351
359
  private connectEventsByAccount = new Map<string, number>();
352
360
  private inboundEventsByAccount = new Map<string, number>();
353
361
  private activityEventsByAccount = new Map<string, number>();
@@ -840,8 +848,21 @@ class BncrBridgeRuntime {
840
848
  return BNCR_DEBUG_VERBOSE;
841
849
  }
842
850
 
843
- startService = async (ctx: OpenClawPluginServiceContext, debug?: boolean) => {
844
- await startBncrBridgeService(
851
+ getRuntimeObservation() {
852
+ const activeChannelAccounts = this.channelAccountWorkers.size;
853
+ return {
854
+ serviceRunning: this.serviceStarted,
855
+ channelRunning: activeChannelAccounts > 0,
856
+ activeChannelAccounts,
857
+ };
858
+ }
859
+
860
+ startService = async (
861
+ ctx: OpenClawPluginServiceContext,
862
+ debug?: boolean,
863
+ isStartCurrent?: () => boolean,
864
+ ) => {
865
+ const started = await startBncrBridgeService(
845
866
  {
846
867
  bridgeId: this.bridgeId,
847
868
  setStopped: (value) => {
@@ -865,11 +886,19 @@ class BncrBridgeRuntime {
865
886
  (((cfg as BncrChannelConfigRoot | null | undefined)?.channels || null)?.[CHANNEL_ID] as
866
887
  | Record<string, unknown>
867
888
  | undefined) || {},
889
+ resumeOutboxDrain: () => {
890
+ if (this.outbox.size === 0 || this.connections.size === 0) return;
891
+ this.schedulePushDrain(0);
892
+ },
893
+ isStartCurrent,
868
894
  },
869
895
  ctx,
870
896
  debug,
871
897
  );
898
+ if (!started || this.stopped || !(isStartCurrent?.() ?? true)) return false;
872
899
  this.startHistoryShardWorker();
900
+ this.serviceStarted = true;
901
+ return true;
873
902
  };
874
903
 
875
904
  stopService = async () => {
@@ -887,6 +916,7 @@ class BncrBridgeRuntime {
887
916
  }
888
917
 
889
918
  private cleanupRuntimeWaitersAndTimers(reason: string) {
919
+ this.serviceStarted = false;
890
920
  this.stopHistoryShardWorker(reason);
891
921
  this.clientRpcRuntime.shutdown();
892
922
  cleanupBncrBridgeRuntime(
@@ -27,12 +27,11 @@ type BncrOutboundSessionRoute = ChannelOutboundSessionRoute & BncrChannelRouteRe
27
27
  type ResolveBncrOutboundSessionRouteParams = {
28
28
  cfg: BncrChannelConfigRoot;
29
29
  channel: string;
30
- agentId: string;
31
30
  accountId?: string;
32
31
  target: string;
33
32
  resolvedTarget?: { to?: string } | null;
34
33
  threadId?: string | number | null;
35
- canonicalAgentId: string;
34
+ resolveAgentId: (route: BncrRoute) => string;
36
35
  resolveRouteBySession?: (raw: string, accountId: string) => BncrRoute | null;
37
36
  };
38
37
 
@@ -76,15 +75,14 @@ export function resolveBncrOutboundSessionRoute(params: ResolveBncrOutboundSessi
76
75
 
77
76
  if (!route) return null;
78
77
 
79
- const canonicalAgentId =
80
- asString(params.canonicalAgentId).trim() || asString(params.agentId).trim() || 'main';
78
+ const agentId = asString(params.resolveAgentId(route)).trim() || 'public';
81
79
  const peerId = routeScopeToHex(route);
82
- const sessionKey = buildCanonicalBncrSessionKey(route, canonicalAgentId);
80
+ const sessionKey = buildCanonicalBncrSessionKey(route, agentId);
83
81
  const displayTo = formatDisplayScope(route);
84
82
 
85
83
  const built = buildOpenClawChannelOutboundSessionRoute({
86
84
  cfg: params.cfg,
87
- agentId: canonicalAgentId,
85
+ agentId,
88
86
  channel: params.channel,
89
87
  accountId: params.accountId,
90
88
  peer: {
@@ -59,13 +59,15 @@ export function startBncrBridgeService(
59
59
  getChannelConfigRoot: (
60
60
  cfg: BncrChannelConfigRoot,
61
61
  ) => BncrChannelPolicyConfig | null | undefined;
62
+ resumeOutboxDrain?: () => void;
63
+ isStartCurrent?: () => boolean;
62
64
  },
63
65
  ctx: OpenClawPluginServiceContext,
64
66
  debug?: boolean,
65
67
  ) {
66
- runtime.setStopped(false);
67
68
  runtime.setStatePath(path.join(ctx.stateDir, 'bncr-bridge-state.json'));
68
69
  return (async () => {
70
+ const isStartCurrent = () => runtime.isStartCurrent?.() ?? true;
69
71
  try {
70
72
  const cfg = runtime.getRuntimeConfig();
71
73
  runtime.initializeCanonicalAgentId(cfg);
@@ -76,8 +78,10 @@ export function startBncrBridgeService(
76
78
  // ignore startup canonical agent initialization errors
77
79
  }
78
80
  await runtime.loadState();
81
+ if (!isStartCurrent()) return false;
79
82
  if (process.env.BNCR_SQLITE_CUTOVER === '1' && runtime.cutoverToSqlite) {
80
83
  const cutover = await runtime.cutoverToSqlite();
84
+ if (!isStartCurrent()) return false;
81
85
  runtime.logInfo(
82
86
  'sqlite',
83
87
  `cutover completed backup=${cutover.backupPath || 'none'} storeMode=${cutover.storeMode}`,
@@ -85,6 +89,8 @@ export function startBncrBridgeService(
85
89
  }
86
90
  if (typeof debug === 'boolean') runtime.setDebugFlag(debug);
87
91
  await runtime.refreshDebugFlagFromConfig({ forceLog: true });
92
+ if (!isStartCurrent()) return false;
93
+ runtime.setStopped(false);
88
94
  const bootDiag = runtime.buildIntegratedDiagnostics(BNCR_DEFAULT_ACCOUNT_ID);
89
95
  runtime.logInfo(
90
96
  'startup',
@@ -95,7 +101,12 @@ export function startBncrBridgeService(
95
101
  `service started bridge=${runtime.bridgeId} diag.ok=${bootDiag.regression.ok} routes=${bootDiag.regression.totalKnownRoutes} pending=${bootDiag.health.pending} dead=${bootDiag.health.deadLetter}`,
96
102
  { debugOnly: true },
97
103
  );
98
- })();
104
+ runtime.resumeOutboxDrain?.();
105
+ return true;
106
+ })().catch((error) => {
107
+ runtime.setStopped(true);
108
+ throw error;
109
+ });
99
110
  }
100
111
 
101
112
  export function cleanupBncrBridgeRuntime(
@@ -34,6 +34,7 @@ export type BncrChannelPluginBridge = {
34
34
  channelId: string;
35
35
  peer: { kind: 'direct'; id: string };
36
36
  }) => string;
37
+ resolveOutboundAgentId: (route: BncrRoute) => string;
37
38
  resolveRouteBySession: (raw: string, accountId: string) => BncrRoute | null;
38
39
  getChannelSummary: (defaultAccountId: string) => Record<string, unknown>;
39
40
  getAccountRuntimeSnapshot: (accountId: string) => BncrStatusRuntimeSnapshot;
@@ -90,14 +91,7 @@ export function createBncrChannelPluginBridgeGroup(runtime: {
90
91
  const getMessagingBridge = () => {
91
92
  const bridge = runtime.getBridge();
92
93
  return {
93
- canonicalAgentId: undefined,
94
- ensureCanonicalAgentId: (params: { cfg: BncrChannelConfigRoot; accountId: string }) =>
95
- bridge.ensureCanonicalAgentId({
96
- cfg: params.cfg,
97
- accountId: params.accountId,
98
- channelId: runtime.channelId,
99
- peer: { kind: 'direct', id: params.accountId },
100
- }),
94
+ resolveOutboundAgentId: (route: BncrRoute) => bridge.resolveOutboundAgentId(route),
101
95
  resolveRouteBySession: (raw: string, accountId: string) =>
102
96
  bridge.resolveRouteBySession(raw, accountId),
103
97
  };
@@ -3,13 +3,13 @@ import { Type } from 'typebox';
3
3
  import { listAccountIds, normalizeAccountId, resolveAccount } from '../core/accounts.ts';
4
4
  import { BncrConfigSchema } from '../core/config-schema.ts';
5
5
  import { resolveBncrChannelPolicy } from '../core/policy.ts';
6
+ import type { BncrRoute } from '../core/types.ts';
6
7
  import type { ReplyPayloadInput } from '../messaging/outbound/reply-enqueue.ts';
7
8
  import type { OutboundReplyTargetPolicy } from '../messaging/outbound/reply-target-policy.ts';
8
9
  import type { OpenClawChannelToolSend, openClawJsonResult } from '../openclaw/sdk-helpers.ts';
9
10
  import { readOpenClawStringParam } from '../openclaw/sdk-helpers.ts';
10
11
  import type { BncrBridgeCallBridge } from './bridge-call.ts';
11
12
  import type {
12
- BncrChannelConfigRoot,
13
13
  BncrChannelSendContext,
14
14
  BncrMessageToolSchemaContribution,
15
15
  BncrSceneRecord,
@@ -81,8 +81,7 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
81
81
  };
82
82
  getOutboundBridge: () => BncrOutboundBridge;
83
83
  getMessagingBridge: () => {
84
- canonicalAgentId: undefined;
85
- ensureCanonicalAgentId: (params: { cfg: BncrChannelConfigRoot; accountId: string }) => string;
84
+ resolveOutboundAgentId: (route: BncrRoute) => string;
86
85
  resolveRouteBySession: (raw: string, accountId: string) => BncrVerifiedTarget['route'] | null;
87
86
  };
88
87
  getStatusBridge: () => {
@@ -15,8 +15,7 @@ function formatBncrHumanDisplay(route: BncrRoute): string {
15
15
  }
16
16
 
17
17
  type BncrMessagingRuntimeBridge = {
18
- canonicalAgentId?: string;
19
- ensureCanonicalAgentId: (params: { cfg: BncrChannelConfigRoot; accountId: string }) => string;
18
+ resolveOutboundAgentId: (route: BncrRoute) => string;
20
19
  resolveRouteBySession: (raw: string, accountId: string) => BncrRoute | null;
21
20
  };
22
21
 
@@ -44,6 +43,7 @@ type BncrMessagingSessionTargetArgs = {
44
43
  };
45
44
 
46
45
  type BncrMessagingOutboundSessionRouteArgs = {
46
+ /** Host-reported owner. BNCR resolves ownership from its persisted scene registry instead. */
47
47
  agentId: string;
48
48
  target: string;
49
49
  resolvedTarget?: { to?: string } | null;
@@ -99,21 +99,9 @@ function resolveMessagingAccountId(accountId: unknown) {
99
99
  return normalizeAccountId(asSanitizedString(accountId || BNCR_DEFAULT_ACCOUNT_ID));
100
100
  }
101
101
 
102
- function resolveMessagingCanonicalAgentId(
103
- runtimeBridge: BncrMessagingRuntimeBridge,
104
- cfg: BncrChannelConfigRoot,
105
- accountId: string,
106
- ) {
107
- return runtimeBridge.canonicalAgentId || runtimeBridge.ensureCanonicalAgentId({ cfg, accountId });
108
- }
109
-
110
- export function createBncrMessagingExplicitTargetParser(
111
- getBridge: () => BncrMessagingRuntimeBridge,
112
- ) {
102
+ export function createBncrMessagingExplicitTargetParser() {
113
103
  return ({ raw }: BncrMessagingExplicitTargetArgs) => {
114
- const runtimeBridge = getBridge();
115
- const canonicalAgentId = runtimeBridge.canonicalAgentId || 'main';
116
- const parsed = parseExplicitTarget(asSanitizedString(raw).trim(), { canonicalAgentId });
104
+ const parsed = parseExplicitTarget(asSanitizedString(raw).trim());
117
105
  if (!parsed) return null;
118
106
  const chatType: ChatType = parsed.route?.groupId ? 'group' : 'direct';
119
107
  return {
@@ -125,16 +113,12 @@ export function createBncrMessagingExplicitTargetParser(
125
113
  };
126
114
  }
127
115
 
128
- export function createBncrMessagingSessionTargetResolver(
129
- getBridge: () => BncrMessagingRuntimeBridge,
130
- ) {
116
+ export function createBncrMessagingSessionTargetResolver() {
131
117
  return ({ id }: BncrMessagingSessionTargetArgs) => {
132
118
  const raw = asSanitizedString(id).trim();
133
119
  if (!raw) return undefined;
134
- const runtimeBridge = getBridge();
135
- const canonicalAgentId = runtimeBridge.canonicalAgentId || 'main';
136
120
 
137
- const parsed = parseExplicitTarget(raw, { canonicalAgentId });
121
+ const parsed = parseExplicitTarget(raw);
138
122
  if (!parsed) {
139
123
  return raw || undefined;
140
124
  }
@@ -146,17 +130,10 @@ export function createBncrMessagingOutboundSessionRouteResolver(
146
130
  getBridge: () => BncrMessagingRuntimeBridge,
147
131
  ) {
148
132
  return (params: BncrMessagingOutboundSessionRouteArgs) => {
149
- const accountId = resolveMessagingAccountId(params?.accountId);
150
133
  const runtimeBridge = getBridge();
151
- const canonicalAgentId = resolveMessagingCanonicalAgentId(
152
- runtimeBridge,
153
- params?.cfg,
154
- accountId,
155
- );
156
134
  return resolveBncrOutboundSessionRoute({
157
135
  channel: 'bncr',
158
136
  cfg: params.cfg,
159
- agentId: params.agentId,
160
137
  accountId: params.accountId ?? undefined,
161
138
  target: params.target,
162
139
  resolvedTarget: params.resolvedTarget,
@@ -164,7 +141,7 @@ export function createBncrMessagingOutboundSessionRouteResolver(
164
141
  params.threadId === null || params.threadId === undefined
165
142
  ? undefined
166
143
  : asSanitizedString(params.threadId),
167
- canonicalAgentId,
144
+ resolveAgentId: (route) => runtimeBridge.resolveOutboundAgentId(route),
168
145
  resolveRouteBySession: (raw: string, acc: string) =>
169
146
  runtimeBridge.resolveRouteBySession(raw, acc),
170
147
  });
@@ -175,9 +152,9 @@ export function createBncrMessagingSurface(getBridge: () => BncrMessagingRuntime
175
152
  return {
176
153
  // 接收任意标签输入;不在 normalize 阶段做格式门槛,统一下沉到发送前验证。
177
154
  normalizeTarget: normalizeBncrMessagingTarget,
178
- parseExplicitTarget: createBncrMessagingExplicitTargetParser(getBridge),
155
+ parseExplicitTarget: createBncrMessagingExplicitTargetParser(),
179
156
  formatTargetDisplay: formatBncrMessagingTargetDisplay,
180
- resolveSessionTarget: createBncrMessagingSessionTargetResolver(getBridge),
157
+ resolveSessionTarget: createBncrMessagingSessionTargetResolver(),
181
158
  resolveOutboundSessionRoute: createBncrMessagingOutboundSessionRouteResolver(getBridge),
182
159
  targetResolver: createBncrMessagingTargetResolver(getBridge),
183
160
  };
@@ -1,3 +1,5 @@
1
+ import type { BncrRoute } from '../core/types.ts';
2
+ import { asString } from '../core/value-sanitize.ts';
1
3
  import type {
2
4
  BncrGroupReplyMode,
3
5
  BncrSceneKind,
@@ -30,10 +32,19 @@ function defaultGroupReplyMode(kind: BncrSceneKind): BncrGroupReplyMode | undefi
30
32
  return kind === 'group' ? 'admin' : undefined;
31
33
  }
32
34
 
35
+ export function buildRouteSceneKey(route: BncrRoute): string {
36
+ const platform = asString(route.platform).trim();
37
+ const groupId = asString(route.groupId).trim() || '0';
38
+ const userId = asString(route.userId).trim() || '0';
39
+ return groupId !== '0' ? `${platform}:${groupId}` : `${platform}:${userId}`;
40
+ }
41
+
33
42
  export function buildSceneKey(parsed: ParsedInbound): string {
34
- return parsed.peer.kind === 'group'
35
- ? `${parsed.platform}:${parsed.groupId}`
36
- : `${parsed.platform}:${parsed.userId}`;
43
+ return buildRouteSceneKey({
44
+ platform: parsed.platform,
45
+ groupId: parsed.groupId || '0',
46
+ userId: parsed.userId || '0',
47
+ });
37
48
  }
38
49
 
39
50
  function buildSceneRecord(args: {