@xmoxmo/bncr 0.1.5 → 0.1.7

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xmoxmo/bncr",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
package/src/channel.ts CHANGED
@@ -38,9 +38,11 @@ import {
38
38
  import {
39
39
  buildCanonicalBncrSessionKey,
40
40
  formatDisplayScope,
41
+ formatTargetDisplay,
41
42
  isLowerHex,
42
43
  normalizeInboundSessionKey,
43
44
  normalizeStoredSessionKey,
45
+ parseExplicitTarget,
44
46
  parseRouteFromDisplayScope,
45
47
  parseRouteFromHexScope,
46
48
  parseRouteFromScope,
@@ -65,6 +67,11 @@ import {
65
67
  resolveBncrOutboundMessageType,
66
68
  } from './messaging/outbound/media.ts';
67
69
  import { sendBncrMedia, sendBncrText } from './messaging/outbound/send.ts';
70
+ import { resolveBncrOutboundSessionRoute } from './messaging/outbound/session-route.ts';
71
+ import {
72
+ looksLikeBncrExplicitTarget,
73
+ resolveBncrOutboundTarget,
74
+ } from './messaging/outbound/target-resolver.ts';
68
75
  const BRIDGE_VERSION = 2;
69
76
  const BNCR_PUSH_EVENT = 'bncr.push';
70
77
  const CONNECT_TTL_MS = 120_000;
@@ -502,7 +509,9 @@ class BncrBridgeRuntime {
502
509
  const summary = this.buildRegisterTraceSummary();
503
510
  if (summary.postWarmupRegisterCount > 0) this.captureDriftSnapshot(summary);
504
511
 
505
- this.api.logger.info?.(`[bncr-register-trace] ${JSON.stringify(trace)}`);
512
+ if (BNCR_DEBUG_VERBOSE) {
513
+ this.api.logger.info?.(`[bncr-register-trace] ${JSON.stringify(trace)}`);
514
+ }
506
515
  }
507
516
 
508
517
  private createLeaseId() {
@@ -1554,77 +1563,35 @@ class BncrBridgeRuntime {
1554
1563
  );
1555
1564
  }
1556
1565
 
1557
- const wantedRouteKey = routeKey(acc, route);
1558
- let best: { sessionKey: string; route: BncrRoute; updatedAt: number } | null = null;
1559
-
1560
- if (BNCR_DEBUG_VERBOSE) {
1561
- this.api.logger.info?.(
1562
- `[bncr-target-incoming-route] raw=${raw} accountId=${acc} route=${JSON.stringify(route)}`,
1563
- );
1564
- this.api.logger.info?.(
1565
- `[bncr-target-incoming-sessionRoutes] raw=${raw} accountId=${acc} sessionRoutes=${JSON.stringify(this.sessionRoutes.entries())}`,
1566
- );
1567
- }
1568
-
1569
- for (const [key, info] of this.sessionRoutes.entries()) {
1570
- if (normalizeAccountId(info.accountId) !== acc) continue;
1571
- const parsed = parseStrictBncrSessionKey(key);
1572
- if (!parsed) continue;
1573
- if (routeKey(acc, parsed.route) !== wantedRouteKey) continue;
1574
-
1575
- const updatedAt = Number(info.updatedAt || 0);
1576
- if (!best || updatedAt >= best.updatedAt) {
1577
- best = {
1578
- sessionKey: key,
1579
- route: parsed.route,
1580
- updatedAt,
1581
- };
1582
- }
1583
- }
1584
-
1585
- if (!best) {
1586
- const updatedAt = 0;
1587
- const canonicalAgentId =
1588
- this.canonicalAgentId ||
1589
- this.ensureCanonicalAgentId({
1590
- cfg: this.api.runtime.config?.get?.() || {},
1591
- accountId: acc,
1592
- channelId: CHANNEL_ID,
1593
- peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
1594
- });
1595
- best = {
1596
- sessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId),
1597
- route,
1598
- updatedAt,
1599
- };
1600
- }
1566
+ const canonicalAgentId =
1567
+ this.canonicalAgentId ||
1568
+ this.ensureCanonicalAgentId({
1569
+ cfg: this.api.runtime.config?.get?.() || {},
1570
+ accountId: acc,
1571
+ channelId: CHANNEL_ID,
1572
+ peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
1573
+ });
1574
+ const verified = {
1575
+ sessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId),
1576
+ route,
1577
+ displayScope: formatDisplayScope(route),
1578
+ };
1601
1579
 
1602
1580
  if (BNCR_DEBUG_VERBOSE) {
1603
1581
  this.api.logger.info?.(
1604
- `[bncr-target-incoming-best] raw=${raw} accountId=${acc} best=${JSON.stringify(best)}`,
1582
+ `[bncr-target-incoming-canonical] raw=${raw} accountId=${acc} verified=${JSON.stringify(verified)}`,
1605
1583
  );
1606
1584
  }
1607
1585
 
1608
- if (!best) {
1609
- this.api.logger.warn?.(
1610
- `[bncr-target-miss] raw=${raw} accountId=${acc} sessionRoutes=${this.sessionRoutes.size}`,
1611
- );
1612
- throw new Error(`bncr target not found in known sessions: ${raw}`);
1613
- }
1614
-
1615
1586
  // 发送链路命中目标时,同步刷新 lastSession,避免状态页显示过期会话。
1616
1587
  this.lastSessionByAccount.set(acc, {
1617
- sessionKey: best.sessionKey,
1618
- scope: formatDisplayScope(best.route),
1588
+ sessionKey: verified.sessionKey,
1589
+ scope: verified.displayScope,
1619
1590
  updatedAt: now(),
1620
1591
  });
1621
1592
  this.scheduleSave();
1622
1593
 
1623
- return {
1624
- sessionKey: best.sessionKey,
1625
- route: best.route,
1626
- displayScope: formatDisplayScope(best.route),
1627
- };
1594
+ return verified;
1628
1595
  }
1629
1596
 
1630
1597
  private markActivity(accountId: string, at = now()) {
@@ -3068,9 +3035,68 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
3068
3035
  const input = asString(raw).trim();
3069
3036
  return input || undefined;
3070
3037
  },
3038
+ parseExplicitTarget: ({ raw, accountId, cfg }: any) => {
3039
+ const resolvedAccountId = normalizeAccountId(
3040
+ asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
3041
+ );
3042
+ const canonicalAgentId =
3043
+ bridge.canonicalAgentId ||
3044
+ bridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
3045
+ return parseExplicitTarget(asString(raw).trim(), { canonicalAgentId });
3046
+ },
3047
+ formatTargetDisplay: ({ target }: any) => {
3048
+ return formatTargetDisplay(target);
3049
+ },
3050
+ resolveSessionTarget: ({ id, accountId, cfg }: any) => {
3051
+ const raw = asString(id).trim();
3052
+ if (!raw) return undefined;
3053
+ const resolvedAccountId = normalizeAccountId(
3054
+ asString(accountId || BNCR_DEFAULT_ACCOUNT_ID),
3055
+ );
3056
+ const canonicalAgentId =
3057
+ bridge.canonicalAgentId ||
3058
+ bridge.ensureCanonicalAgentId({ cfg, accountId: resolvedAccountId });
3059
+
3060
+ let parsed = parseExplicitTarget(raw, { canonicalAgentId });
3061
+ if (!parsed) {
3062
+ const route = bridge.resolveRouteBySession(raw, resolvedAccountId);
3063
+ if (route) {
3064
+ parsed = parseExplicitTarget(formatDisplayScope(route), { canonicalAgentId });
3065
+ }
3066
+ }
3067
+ return parsed?.displayScope || undefined;
3068
+ },
3069
+ resolveOutboundSessionRoute: (params: any) => {
3070
+ const accountId = normalizeAccountId(
3071
+ asString(params?.accountId || BNCR_DEFAULT_ACCOUNT_ID),
3072
+ );
3073
+ const canonicalAgentId =
3074
+ bridge.canonicalAgentId || bridge.ensureCanonicalAgentId({ cfg: params?.cfg, accountId });
3075
+ return resolveBncrOutboundSessionRoute({
3076
+ ...params,
3077
+ canonicalAgentId,
3078
+ resolveRouteBySession: (raw: string, acc: string) =>
3079
+ bridge.resolveRouteBySession(raw, acc),
3080
+ });
3081
+ },
3071
3082
  targetResolver: {
3072
3083
  looksLikeId: (raw: string, normalized?: string) => {
3073
- return Boolean(asString(normalized || raw).trim());
3084
+ return looksLikeBncrExplicitTarget(asString(normalized || raw).trim());
3085
+ },
3086
+ resolveTarget: async ({ accountId, input, normalized }) => {
3087
+ const resolved = resolveBncrOutboundTarget({
3088
+ target: asString(normalized || input).trim(),
3089
+ accountId: normalizeAccountId(asString(accountId || BNCR_DEFAULT_ACCOUNT_ID)),
3090
+ resolveRouteBySession: (raw: string, acc: string) =>
3091
+ this.resolveRouteBySession(raw, acc),
3092
+ });
3093
+ if (!resolved) return null;
3094
+ return {
3095
+ to: resolved.displayScope,
3096
+ kind: resolved.kind,
3097
+ display: resolved.displayScope,
3098
+ source: 'normalized' as const,
3099
+ };
3074
3100
  },
3075
3101
  hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent:<agentId>:bncr:direct:<hex>',
3076
3102
  },
@@ -1,6 +1,24 @@
1
1
  import type { BncrRoute } from './types.ts';
2
2
 
3
3
  export type BncrSessionKind = 'direct' | 'group';
4
+ export type BncrExplicitTarget = {
5
+ raw: string;
6
+ normalized: string;
7
+ source:
8
+ | 'display-scope'
9
+ | 'strict-session-key'
10
+ | 'legacy-session-key'
11
+ | 'hex-scope'
12
+ | 'route-scope';
13
+ kind: BncrSessionKind;
14
+ chatType: 'direct';
15
+ displayScope: string;
16
+ route: BncrRoute;
17
+ canonicalSessionKey?: string;
18
+ platform: string;
19
+ userId: string;
20
+ groupId?: string;
21
+ };
4
22
 
5
23
  function asString(v: unknown, fallback = ''): string {
6
24
  if (typeof v === 'string') return v;
@@ -54,6 +72,85 @@ export function buildDisplayScopeCandidates(route: BncrRoute): string[] {
54
72
  return Array.from(new Set(candidates.map((x) => asString(x).trim()).filter(Boolean)));
55
73
  }
56
74
 
75
+ export function formatTargetDisplay(
76
+ input: BncrRoute | BncrExplicitTarget | null | undefined,
77
+ ): string {
78
+ if (!input) return '';
79
+ const route = parseRouteLike((input as any)?.route) || parseRouteLike(input);
80
+ if (!route) return '';
81
+ return formatDisplayScope(route);
82
+ }
83
+
84
+ export function parseExplicitTarget(
85
+ input: string,
86
+ options?: { canonicalAgentId?: string | null },
87
+ ): BncrExplicitTarget | null {
88
+ const raw = asString(input).trim();
89
+ if (!raw) return null;
90
+
91
+ const canonicalAgentId = asString(options?.canonicalAgentId).trim() || undefined;
92
+ let route: BncrRoute | null = null;
93
+ let source: BncrExplicitTarget['source'] | null = null;
94
+
95
+ const strict = parseStrictBncrSessionKey(raw);
96
+ if (strict?.route) {
97
+ route = strict.route;
98
+ source = 'strict-session-key';
99
+ }
100
+
101
+ if (!route) {
102
+ const displayRoute = parseRouteFromDisplayScope(raw);
103
+ if (displayRoute) {
104
+ route = displayRoute;
105
+ source = 'display-scope';
106
+ }
107
+ }
108
+
109
+ if (!route) {
110
+ const legacy = parseLegacySessionKey(raw);
111
+ if (legacy?.route) {
112
+ route = legacy.route;
113
+ source = legacy.source === 'hex' ? 'hex-scope' : 'legacy-session-key';
114
+ }
115
+ }
116
+
117
+ if (!route) {
118
+ const hexRoute = parseRouteFromHexScope(raw);
119
+ if (hexRoute) {
120
+ route = hexRoute;
121
+ source = 'hex-scope';
122
+ }
123
+ }
124
+
125
+ if (!route) {
126
+ const scopedRoute = parseRouteFromScope(raw);
127
+ if (scopedRoute) {
128
+ route = scopedRoute;
129
+ source = 'route-scope';
130
+ }
131
+ }
132
+
133
+ if (!route || !source) return null;
134
+
135
+ const kind: BncrSessionKind = route.groupId === '0' ? 'direct' : 'group';
136
+ const displayScope = formatDisplayScope(route);
137
+ return {
138
+ raw,
139
+ normalized: displayScope,
140
+ source,
141
+ kind,
142
+ chatType: 'direct',
143
+ displayScope,
144
+ route,
145
+ ...(canonicalAgentId
146
+ ? { canonicalSessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId) }
147
+ : {}),
148
+ platform: route.platform,
149
+ userId: route.userId,
150
+ ...(route.groupId === '0' ? {} : { groupId: route.groupId }),
151
+ };
152
+ }
153
+
57
154
  export function isLowerHex(input: string): boolean {
58
155
  const raw = asString(input).trim();
59
156
  return !!raw && /^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0;
@@ -55,7 +55,7 @@ export function buildBncrMediaOutboundFrame(params: {
55
55
  mediaMsg: string;
56
56
  fileName: string;
57
57
  hintedType?: string;
58
- kind?: 'block' | 'final';
58
+ kind?: 'tool' | 'block' | 'final';
59
59
  now: number;
60
60
  }) {
61
61
  return {
@@ -0,0 +1,73 @@
1
+ import { buildChannelOutboundSessionRoute } from 'openclaw/plugin-sdk/core';
2
+ import {
3
+ buildCanonicalBncrSessionKey,
4
+ formatDisplayScope,
5
+ parseRouteFromDisplayScope,
6
+ parseStrictBncrSessionKey,
7
+ routeScopeToHex,
8
+ } from '../../core/targets.ts';
9
+ import type { BncrRoute } from '../../core/types.ts';
10
+
11
+ type ResolveBncrOutboundSessionRouteParams = {
12
+ cfg: any;
13
+ channel: string;
14
+ agentId: string;
15
+ accountId?: string;
16
+ target: string;
17
+ resolvedTarget?: { to?: string } | null;
18
+ threadId?: string;
19
+ canonicalAgentId: string;
20
+ resolveRouteBySession?: (raw: string, accountId: string) => BncrRoute | null;
21
+ };
22
+
23
+ function asString(v: unknown, fallback = ''): string {
24
+ if (typeof v === 'string') return v;
25
+ if (v == null) return fallback;
26
+ return String(v);
27
+ }
28
+
29
+ export function resolveBncrOutboundSessionRoute(params: ResolveBncrOutboundSessionRouteParams) {
30
+ const raw = asString(params.resolvedTarget?.to || params.target).trim();
31
+ if (!raw) return null;
32
+
33
+ let route: BncrRoute | null = null;
34
+
35
+ const strict = parseStrictBncrSessionKey(raw);
36
+ if (strict) {
37
+ route = strict.route;
38
+ } else {
39
+ route = parseRouteFromDisplayScope(raw);
40
+ if (!route && params.accountId && params.resolveRouteBySession) {
41
+ route = params.resolveRouteBySession(raw, params.accountId);
42
+ }
43
+ }
44
+
45
+ if (!route) return null;
46
+
47
+ const canonicalAgentId =
48
+ asString(params.canonicalAgentId).trim() || asString(params.agentId).trim() || 'main';
49
+ const peerId = routeScopeToHex(route);
50
+ const sessionKey = buildCanonicalBncrSessionKey(route, canonicalAgentId);
51
+ const displayTo = formatDisplayScope(route);
52
+
53
+ const built = buildChannelOutboundSessionRoute({
54
+ cfg: params.cfg,
55
+ agentId: canonicalAgentId,
56
+ channel: params.channel,
57
+ accountId: params.accountId,
58
+ peer: {
59
+ kind: 'direct',
60
+ id: peerId,
61
+ },
62
+ chatType: 'direct',
63
+ from: displayTo,
64
+ to: displayTo,
65
+ ...(params.threadId !== undefined ? { threadId: params.threadId } : {}),
66
+ });
67
+
68
+ return {
69
+ ...built,
70
+ sessionKey,
71
+ baseSessionKey: sessionKey,
72
+ };
73
+ }
@@ -0,0 +1,56 @@
1
+ import {
2
+ formatDisplayScope,
3
+ parseRouteFromDisplayScope,
4
+ parseStrictBncrSessionKey,
5
+ } from '../../core/targets.ts';
6
+ import type { BncrRoute } from '../../core/types.ts';
7
+
8
+ type ResolveBncrOutboundTargetParams = {
9
+ target: string;
10
+ accountId?: string | null;
11
+ resolveRouteBySession?: (raw: string, accountId: string) => BncrRoute | null;
12
+ };
13
+
14
+ function asString(v: unknown, fallback = ''): string {
15
+ if (typeof v === 'string') return v;
16
+ if (v == null) return fallback;
17
+ return String(v);
18
+ }
19
+
20
+ export function looksLikeBncrExplicitTarget(input: string): boolean {
21
+ const raw = asString(input).trim();
22
+ if (!raw) return false;
23
+ return Boolean(parseRouteFromDisplayScope(raw) || parseStrictBncrSessionKey(raw));
24
+ }
25
+
26
+ export function resolveBncrOutboundTarget(params: ResolveBncrOutboundTargetParams): {
27
+ route: BncrRoute;
28
+ displayScope: string;
29
+ kind: 'user' | 'group';
30
+ } | null {
31
+ const raw = asString(params.target).trim();
32
+ if (!raw) return null;
33
+
34
+ let route: BncrRoute | null = null;
35
+
36
+ const strict = parseStrictBncrSessionKey(raw);
37
+ if (strict?.route) {
38
+ route = strict.route;
39
+ }
40
+
41
+ if (!route) {
42
+ route = parseRouteFromDisplayScope(raw);
43
+ }
44
+
45
+ if (!route && params.accountId && params.resolveRouteBySession) {
46
+ route = params.resolveRouteBySession(raw, params.accountId);
47
+ }
48
+
49
+ if (!route) return null;
50
+
51
+ return {
52
+ route,
53
+ displayScope: formatDisplayScope(route),
54
+ kind: route.groupId === '0' ? 'user' : 'group',
55
+ };
56
+ }