@xmoxmo/bncr 0.1.6 → 0.1.8
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/index.ts +18 -11
- package/package.json +1 -1
- package/src/channel.ts +382 -268
- package/src/core/logging.ts +65 -0
- package/src/core/targets.ts +97 -0
- package/src/messaging/inbound/commands.ts +4 -2
- package/src/messaging/inbound/dispatch.ts +4 -3
- package/src/messaging/outbound/media.ts +3 -1
- package/src/messaging/outbound/send.ts +11 -1
- package/src/messaging/outbound/session-route.ts +73 -0
- package/src/messaging/outbound/target-resolver.ts +56 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type BncrLogLevel = 'info' | 'warn' | 'error';
|
|
2
|
+
export type BncrLogOptions = { debugOnly?: boolean };
|
|
3
|
+
|
|
4
|
+
const BNCR_PREFIX = '[bncr]';
|
|
5
|
+
|
|
6
|
+
type DebugGate = () => boolean;
|
|
7
|
+
|
|
8
|
+
type ConsoleMethod = 'log' | 'warn' | 'error';
|
|
9
|
+
|
|
10
|
+
function resolveConsoleMethod(level: BncrLogLevel): ConsoleMethod {
|
|
11
|
+
switch (level) {
|
|
12
|
+
case 'warn':
|
|
13
|
+
return 'warn';
|
|
14
|
+
case 'error':
|
|
15
|
+
return 'error';
|
|
16
|
+
default:
|
|
17
|
+
return 'log';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function emitConsole(method: ConsoleMethod, line: string) {
|
|
22
|
+
if (method === 'warn') {
|
|
23
|
+
console.warn(line);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (method === 'error') {
|
|
27
|
+
console.error(line);
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
console.log(line);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function normalizeBncrLogLine(raw: string | undefined) {
|
|
34
|
+
const text = String(raw || '').trim();
|
|
35
|
+
if (!text) return BNCR_PREFIX;
|
|
36
|
+
return text.startsWith(BNCR_PREFIX) ? text : `${BNCR_PREFIX} ${text}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function formatBncrLogLine(scope: string | undefined, message: string | undefined) {
|
|
40
|
+
const normalizedScope = String(scope || '').trim();
|
|
41
|
+
const normalizedMessage = String(message || '').trim();
|
|
42
|
+
const prefix = normalizedScope ? `${BNCR_PREFIX} ${normalizedScope}` : BNCR_PREFIX;
|
|
43
|
+
return normalizedMessage ? `${prefix} ${normalizedMessage}` : prefix;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function emitBncrLog(
|
|
47
|
+
level: BncrLogLevel,
|
|
48
|
+
scope: string | undefined,
|
|
49
|
+
message: string | undefined,
|
|
50
|
+
options?: BncrLogOptions,
|
|
51
|
+
isDebugEnabled?: DebugGate,
|
|
52
|
+
) {
|
|
53
|
+
if (options?.debugOnly && !(isDebugEnabled?.() ?? false)) return;
|
|
54
|
+
emitConsole(resolveConsoleMethod(level), formatBncrLogLine(scope, message));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function emitBncrLogLine(
|
|
58
|
+
level: BncrLogLevel,
|
|
59
|
+
line: string | undefined,
|
|
60
|
+
options?: BncrLogOptions,
|
|
61
|
+
isDebugEnabled?: DebugGate,
|
|
62
|
+
) {
|
|
63
|
+
if (options?.debugOnly && !(isDebugEnabled?.() ?? false)) return;
|
|
64
|
+
emitConsole(resolveConsoleMethod(level), normalizeBncrLogLine(line));
|
|
65
|
+
}
|
package/src/core/targets.ts
CHANGED
|
@@ -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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
1
2
|
import {
|
|
2
3
|
formatDisplayScope,
|
|
3
4
|
normalizeInboundSessionKey,
|
|
@@ -78,7 +79,7 @@ export async function handleBncrNativeCommand(params: {
|
|
|
78
79
|
const displayTo = formatDisplayScope(route);
|
|
79
80
|
const body = command.body;
|
|
80
81
|
if (!clientId) {
|
|
81
|
-
|
|
82
|
+
emitBncrLogLine('warn', '[bncr] inbound missing clientId for native command identity');
|
|
82
83
|
return { handled: false };
|
|
83
84
|
}
|
|
84
85
|
const senderIdForContext = clientId;
|
|
@@ -119,7 +120,7 @@ export async function handleBncrNativeCommand(params: {
|
|
|
119
120
|
sessionKey,
|
|
120
121
|
ctx: ctxPayload,
|
|
121
122
|
onRecordError: (err: unknown) => {
|
|
122
|
-
|
|
123
|
+
emitBncrLogLine('warn', `[bncr] inbound record native command session failed: ${String(err)}`);
|
|
123
124
|
},
|
|
124
125
|
});
|
|
125
126
|
|
|
@@ -155,6 +156,7 @@ export async function handleBncrNativeCommand(params: {
|
|
|
155
156
|
payload: {
|
|
156
157
|
...payload,
|
|
157
158
|
kind: kind as 'tool' | 'block' | 'final' | undefined,
|
|
159
|
+
replyToId: msgId || undefined,
|
|
158
160
|
},
|
|
159
161
|
});
|
|
160
162
|
},
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { emitBncrLogLine } from '../../core/logging.ts';
|
|
1
2
|
import {
|
|
2
3
|
formatDisplayScope,
|
|
3
4
|
normalizeInboundSessionKey,
|
|
@@ -132,7 +133,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
132
133
|
|
|
133
134
|
const displayTo = formatDisplayScope(route);
|
|
134
135
|
if (!clientId) {
|
|
135
|
-
|
|
136
|
+
emitBncrLogLine('warn', '[bncr] inbound missing clientId for chat identity');
|
|
136
137
|
return {
|
|
137
138
|
accountId,
|
|
138
139
|
sessionKey,
|
|
@@ -171,7 +172,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
171
172
|
sessionKey,
|
|
172
173
|
ctx: ctxPayload,
|
|
173
174
|
onRecordError: (err: unknown) => {
|
|
174
|
-
|
|
175
|
+
emitBncrLogLine('warn', `[bncr] inbound record session failed: ${String(err)}`);
|
|
175
176
|
},
|
|
176
177
|
});
|
|
177
178
|
|
|
@@ -207,7 +208,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
207
208
|
});
|
|
208
209
|
},
|
|
209
210
|
onError: (err: unknown) => {
|
|
210
|
-
|
|
211
|
+
emitBncrLogLine('error', `[bncr] outbound reply failed: ${String(err)}`);
|
|
211
212
|
},
|
|
212
213
|
},
|
|
213
214
|
replyOptions: {
|
|
@@ -55,7 +55,8 @@ 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
|
+
replyToId?: string;
|
|
59
60
|
now: number;
|
|
60
61
|
}) {
|
|
61
62
|
return {
|
|
@@ -63,6 +64,7 @@ export function buildBncrMediaOutboundFrame(params: {
|
|
|
63
64
|
messageId: params.messageId,
|
|
64
65
|
idempotencyKey: params.messageId,
|
|
65
66
|
sessionKey: params.sessionKey,
|
|
67
|
+
replyToId: asString(params.replyToId || '').trim() || undefined,
|
|
66
68
|
message: {
|
|
67
69
|
platform: params.route.platform,
|
|
68
70
|
groupId: params.route.groupId,
|
|
@@ -3,6 +3,7 @@ export async function sendBncrText(params: {
|
|
|
3
3
|
accountId: string;
|
|
4
4
|
to: string;
|
|
5
5
|
text: string;
|
|
6
|
+
replyToId?: string;
|
|
6
7
|
mediaLocalRoots?: readonly string[];
|
|
7
8
|
resolveVerifiedTarget: (
|
|
8
9
|
to: string,
|
|
@@ -13,7 +14,12 @@ export async function sendBncrText(params: {
|
|
|
13
14
|
accountId: string;
|
|
14
15
|
sessionKey: string;
|
|
15
16
|
route: any;
|
|
16
|
-
payload: {
|
|
17
|
+
payload: {
|
|
18
|
+
text?: string;
|
|
19
|
+
mediaUrl?: string;
|
|
20
|
+
mediaUrls?: string[];
|
|
21
|
+
replyToId?: string;
|
|
22
|
+
};
|
|
17
23
|
mediaLocalRoots?: readonly string[];
|
|
18
24
|
}) => Promise<void>;
|
|
19
25
|
createMessageId: () => string;
|
|
@@ -27,6 +33,7 @@ export async function sendBncrText(params: {
|
|
|
27
33
|
route: verified.route,
|
|
28
34
|
payload: {
|
|
29
35
|
text: params.text,
|
|
36
|
+
replyToId: params.replyToId,
|
|
30
37
|
},
|
|
31
38
|
mediaLocalRoots: params.mediaLocalRoots,
|
|
32
39
|
});
|
|
@@ -46,6 +53,7 @@ export async function sendBncrMedia(params: {
|
|
|
46
53
|
mediaUrl?: string;
|
|
47
54
|
asVoice?: boolean;
|
|
48
55
|
audioAsVoice?: boolean;
|
|
56
|
+
replyToId?: string;
|
|
49
57
|
mediaLocalRoots?: readonly string[];
|
|
50
58
|
resolveVerifiedTarget: (
|
|
51
59
|
to: string,
|
|
@@ -62,6 +70,7 @@ export async function sendBncrMedia(params: {
|
|
|
62
70
|
mediaUrls?: string[];
|
|
63
71
|
asVoice?: boolean;
|
|
64
72
|
audioAsVoice?: boolean;
|
|
73
|
+
replyToId?: string;
|
|
65
74
|
};
|
|
66
75
|
mediaLocalRoots?: readonly string[];
|
|
67
76
|
}) => Promise<void>;
|
|
@@ -79,6 +88,7 @@ export async function sendBncrMedia(params: {
|
|
|
79
88
|
mediaUrl: params.mediaUrl || '',
|
|
80
89
|
asVoice: params.asVoice === true ? true : undefined,
|
|
81
90
|
audioAsVoice: params.audioAsVoice === true ? true : undefined,
|
|
91
|
+
replyToId: params.replyToId,
|
|
82
92
|
},
|
|
83
93
|
mediaLocalRoots: params.mediaLocalRoots,
|
|
84
94
|
});
|
|
@@ -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
|
+
}
|