@xmoxmo/bncr 0.1.6 → 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.6",
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;
@@ -1556,77 +1563,35 @@ class BncrBridgeRuntime {
1556
1563
  );
1557
1564
  }
1558
1565
 
1559
- const wantedRouteKey = routeKey(acc, route);
1560
- let best: { sessionKey: string; route: BncrRoute; updatedAt: number } | null = null;
1561
-
1562
- if (BNCR_DEBUG_VERBOSE) {
1563
- this.api.logger.info?.(
1564
- `[bncr-target-incoming-route] raw=${raw} accountId=${acc} route=${JSON.stringify(route)}`,
1565
- );
1566
- this.api.logger.info?.(
1567
- `[bncr-target-incoming-sessionRoutes] raw=${raw} accountId=${acc} sessionRoutes=${JSON.stringify(this.sessionRoutes.entries())}`,
1568
- );
1569
- }
1570
-
1571
- for (const [key, info] of this.sessionRoutes.entries()) {
1572
- if (normalizeAccountId(info.accountId) !== acc) continue;
1573
- const parsed = parseStrictBncrSessionKey(key);
1574
- if (!parsed) continue;
1575
- if (routeKey(acc, parsed.route) !== wantedRouteKey) continue;
1576
-
1577
- const updatedAt = Number(info.updatedAt || 0);
1578
- if (!best || updatedAt >= best.updatedAt) {
1579
- best = {
1580
- sessionKey: key,
1581
- route: parsed.route,
1582
- updatedAt,
1583
- };
1584
- }
1585
- }
1586
-
1587
- if (!best) {
1588
- const updatedAt = 0;
1589
- const canonicalAgentId =
1590
- this.canonicalAgentId ||
1591
- this.ensureCanonicalAgentId({
1592
- cfg: this.api.runtime.config?.get?.() || {},
1593
- accountId: acc,
1594
- channelId: CHANNEL_ID,
1595
- peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
1596
- });
1597
- best = {
1598
- sessionKey: buildCanonicalBncrSessionKey(route, canonicalAgentId),
1599
- route,
1600
- updatedAt,
1601
- };
1602
- }
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
+ };
1603
1579
 
1604
1580
  if (BNCR_DEBUG_VERBOSE) {
1605
1581
  this.api.logger.info?.(
1606
- `[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)}`,
1607
1583
  );
1608
1584
  }
1609
1585
 
1610
- if (!best) {
1611
- this.api.logger.warn?.(
1612
- `[bncr-target-miss] raw=${raw} accountId=${acc} sessionRoutes=${this.sessionRoutes.size}`,
1613
- );
1614
- throw new Error(`bncr target not found in known sessions: ${raw}`);
1615
- }
1616
-
1617
1586
  // 发送链路命中目标时,同步刷新 lastSession,避免状态页显示过期会话。
1618
1587
  this.lastSessionByAccount.set(acc, {
1619
- sessionKey: best.sessionKey,
1620
- scope: formatDisplayScope(best.route),
1588
+ sessionKey: verified.sessionKey,
1589
+ scope: verified.displayScope,
1621
1590
  updatedAt: now(),
1622
1591
  });
1623
1592
  this.scheduleSave();
1624
1593
 
1625
- return {
1626
- sessionKey: best.sessionKey,
1627
- route: best.route,
1628
- displayScope: formatDisplayScope(best.route),
1629
- };
1594
+ return verified;
1630
1595
  }
1631
1596
 
1632
1597
  private markActivity(accountId: string, at = now()) {
@@ -3070,9 +3035,68 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
3070
3035
  const input = asString(raw).trim();
3071
3036
  return input || undefined;
3072
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
+ },
3073
3082
  targetResolver: {
3074
3083
  looksLikeId: (raw: string, normalized?: string) => {
3075
- 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
+ };
3076
3100
  },
3077
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>',
3078
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
+ }