@xmoxmo/bncr 0.4.6 → 0.4.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 +1 -1
- package/src/channel.ts +41 -2
- package/src/core/targets.ts +106 -17
- package/src/messaging/inbound/commands.ts +263 -51
- package/src/messaging/inbound/context-facts.ts +126 -14
- package/src/messaging/inbound/contracts.ts +24 -0
- package/src/messaging/inbound/dispatch-prep.ts +214 -39
- package/src/messaging/inbound/dispatch.ts +71 -5
- package/src/messaging/inbound/gate.ts +56 -86
- package/src/messaging/inbound/group-history.ts +189 -0
- package/src/messaging/inbound/native-command-runtime.ts +77 -61
- package/src/messaging/inbound/native-command.ts +92 -8
- package/src/messaging/inbound/parse.ts +113 -8
- package/src/messaging/inbound/reply-dispatch-serial.ts +62 -0
- package/src/messaging/inbound/reply-dispatch.ts +252 -77
- package/src/messaging/inbound/scene-admin.ts +269 -0
- package/src/messaging/inbound/session-label.ts +122 -13
- package/src/messaging/inbound/session-meta-task.ts +17 -0
- package/src/messaging/inbound/turn-context.ts +184 -71
- package/src/openclaw/channel-runtime-contracts.ts +1 -0
- package/src/plugin/channel-components.ts +34 -1
- package/src/plugin/channel-inbound-helpers.ts +9 -2
- package/src/plugin/channel-runtime-builders-delivery.ts +24 -1
- package/src/plugin/channel-runtime-types.ts +42 -0
- package/src/plugin/file-inbound-init.ts +27 -12
- package/src/plugin/file-inbound-runtime.ts +2 -0
- package/src/plugin/inbound-acceptance.ts +82 -1
- package/src/plugin/inbound-handlers.ts +55 -2
- package/src/plugin/inbound-surface-handlers-group.ts +16 -0
- package/src/plugin/messaging.ts +22 -5
- package/src/plugin/scene-registry.ts +155 -0
- package/src/plugin/state-store.ts +133 -0
- package/src/plugin/state-transient-runtime-group.ts +5 -0
- package/src/plugin/target-runtime.ts +2 -2
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createChannelHistoryWindow,
|
|
3
|
+
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
4
|
+
type HistoryEntry,
|
|
5
|
+
type HistoryMediaEntry,
|
|
6
|
+
} from 'openclaw/plugin-sdk/reply-history';
|
|
7
|
+
import { formatOpenClawAgentEnvelope } from '../../openclaw/reply-runtime.ts';
|
|
8
|
+
import type { BncrInboundApi } from './contracts.ts';
|
|
9
|
+
import type { ParsedInbound } from './dispatch-prep.ts';
|
|
10
|
+
|
|
11
|
+
export type BncrGroupHistoryMap = Map<string, HistoryEntry[]>;
|
|
12
|
+
|
|
13
|
+
type BncrHistoryMediaKind = NonNullable<HistoryMediaEntry['kind']>;
|
|
14
|
+
|
|
15
|
+
function normalizeTextBody(value: string): string {
|
|
16
|
+
return String(value || '')
|
|
17
|
+
.replace(/\s+/g, ' ')
|
|
18
|
+
.trim();
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function buildBncrGroupHistoryKey(parsed: ParsedInbound): string | null {
|
|
22
|
+
if (parsed.peer.kind !== 'group') return null;
|
|
23
|
+
const platform = String(parsed.platform || '').trim();
|
|
24
|
+
const groupId = String(parsed.groupId || '').trim();
|
|
25
|
+
if (!platform || !groupId || groupId === '0') return null;
|
|
26
|
+
return `${platform}:${groupId}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function recordBncrPendingGroupText(args: {
|
|
30
|
+
historyMap: BncrGroupHistoryMap;
|
|
31
|
+
parsed: ParsedInbound;
|
|
32
|
+
senderDisplayName: string;
|
|
33
|
+
bodyText: string;
|
|
34
|
+
}) {
|
|
35
|
+
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
36
|
+
const body = normalizeTextBody(args.bodyText);
|
|
37
|
+
if (!historyKey || !body || args.parsed.msgType !== 'text') return;
|
|
38
|
+
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
39
|
+
historyKey,
|
|
40
|
+
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
41
|
+
entry: {
|
|
42
|
+
sender: args.senderDisplayName,
|
|
43
|
+
body,
|
|
44
|
+
timestamp: Date.now(),
|
|
45
|
+
messageId: args.parsed.msgId,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function inferBncrHistoryMediaKind(args: {
|
|
51
|
+
msgType?: string;
|
|
52
|
+
mediaContentType?: string;
|
|
53
|
+
}): BncrHistoryMediaKind {
|
|
54
|
+
const msgType = String(args.msgType || '')
|
|
55
|
+
.trim()
|
|
56
|
+
.toLowerCase();
|
|
57
|
+
const contentType = String(args.mediaContentType || '')
|
|
58
|
+
.trim()
|
|
59
|
+
.toLowerCase();
|
|
60
|
+
if (msgType === 'image' || contentType.startsWith('image/')) return 'image';
|
|
61
|
+
if (msgType === 'video' || contentType.startsWith('video/')) return 'video';
|
|
62
|
+
if (msgType === 'audio' || msgType === 'voice' || contentType.startsWith('audio/'))
|
|
63
|
+
return 'audio';
|
|
64
|
+
if (msgType === 'file' || msgType === 'document') return 'document';
|
|
65
|
+
if (contentType) return 'document';
|
|
66
|
+
return 'unknown';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function buildBncrHistoryMediaBody(kind: BncrHistoryMediaKind): string {
|
|
70
|
+
return `<media:${kind}>`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function recordBncrPendingGroupMedia(args: {
|
|
74
|
+
historyMap: BncrGroupHistoryMap;
|
|
75
|
+
parsed: ParsedInbound;
|
|
76
|
+
senderDisplayName: string;
|
|
77
|
+
bodyText: string;
|
|
78
|
+
mediaItems?: Array<{
|
|
79
|
+
path: string;
|
|
80
|
+
contentType?: string;
|
|
81
|
+
kind?: BncrHistoryMediaKind;
|
|
82
|
+
}>;
|
|
83
|
+
mediaContentType?: string;
|
|
84
|
+
}) {
|
|
85
|
+
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
86
|
+
if (!historyKey || args.parsed.msgType === 'text') return;
|
|
87
|
+
const normalizedMediaItems = Array.isArray(args.mediaItems) ? args.mediaItems : [];
|
|
88
|
+
const itemKinds = normalizedMediaItems
|
|
89
|
+
.map((item) =>
|
|
90
|
+
String(item?.kind || '')
|
|
91
|
+
.trim()
|
|
92
|
+
.toLowerCase(),
|
|
93
|
+
)
|
|
94
|
+
.filter(Boolean);
|
|
95
|
+
const kind = itemKinds[0]
|
|
96
|
+
? itemKinds.every((candidate) => candidate === itemKinds[0])
|
|
97
|
+
? (itemKinds[0] as BncrHistoryMediaKind)
|
|
98
|
+
: 'document'
|
|
99
|
+
: inferBncrHistoryMediaKind({
|
|
100
|
+
msgType: args.parsed.msgType,
|
|
101
|
+
mediaContentType: args.mediaContentType,
|
|
102
|
+
});
|
|
103
|
+
const body = normalizeTextBody(args.bodyText) || buildBncrHistoryMediaBody(kind);
|
|
104
|
+
if (normalizedMediaItems.length === 0 || kind !== 'image') {
|
|
105
|
+
createChannelHistoryWindow({ historyMap: args.historyMap }).record({
|
|
106
|
+
historyKey,
|
|
107
|
+
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
108
|
+
entry: {
|
|
109
|
+
sender: args.senderDisplayName,
|
|
110
|
+
body,
|
|
111
|
+
timestamp: Date.now(),
|
|
112
|
+
messageId: args.parsed.msgId,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
await createChannelHistoryWindow({ historyMap: args.historyMap }).recordWithMedia({
|
|
118
|
+
historyKey,
|
|
119
|
+
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
120
|
+
entry: {
|
|
121
|
+
sender: args.senderDisplayName,
|
|
122
|
+
body,
|
|
123
|
+
timestamp: Date.now(),
|
|
124
|
+
messageId: args.parsed.msgId,
|
|
125
|
+
},
|
|
126
|
+
messageId: args.parsed.msgId,
|
|
127
|
+
media: normalizedMediaItems.map(
|
|
128
|
+
(item) =>
|
|
129
|
+
({
|
|
130
|
+
path: item.path,
|
|
131
|
+
contentType: item.contentType || args.mediaContentType || 'image/*',
|
|
132
|
+
kind: 'image',
|
|
133
|
+
messageId: args.parsed.msgId,
|
|
134
|
+
}) satisfies HistoryMediaEntry,
|
|
135
|
+
),
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function buildBncrPendingGroupContext(args: {
|
|
140
|
+
api: BncrInboundApi;
|
|
141
|
+
historyMap: BncrGroupHistoryMap;
|
|
142
|
+
parsed: ParsedInbound;
|
|
143
|
+
channelLabel: string;
|
|
144
|
+
currentTimestamp: number;
|
|
145
|
+
previousTimestamp?: unknown;
|
|
146
|
+
envelope?: unknown;
|
|
147
|
+
currentMessage: string;
|
|
148
|
+
}) {
|
|
149
|
+
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
150
|
+
if (!historyKey) return args.currentMessage;
|
|
151
|
+
return createChannelHistoryWindow({ historyMap: args.historyMap }).buildPendingContext({
|
|
152
|
+
historyKey,
|
|
153
|
+
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
154
|
+
currentMessage: args.currentMessage,
|
|
155
|
+
formatEntry: (entry) =>
|
|
156
|
+
formatOpenClawAgentEnvelope(args.api, {
|
|
157
|
+
channel: 'Bncr',
|
|
158
|
+
from: args.channelLabel,
|
|
159
|
+
timestamp: entry.timestamp || args.currentTimestamp,
|
|
160
|
+
previousTimestamp: args.previousTimestamp,
|
|
161
|
+
envelope: args.envelope,
|
|
162
|
+
body: entry.body,
|
|
163
|
+
}),
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function buildBncrInboundHistory(args: {
|
|
168
|
+
historyMap: BncrGroupHistoryMap;
|
|
169
|
+
parsed: ParsedInbound;
|
|
170
|
+
}) {
|
|
171
|
+
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
172
|
+
if (!historyKey) return undefined;
|
|
173
|
+
return createChannelHistoryWindow({ historyMap: args.historyMap }).buildInboundHistory({
|
|
174
|
+
historyKey,
|
|
175
|
+
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export function clearBncrPendingGroupHistory(args: {
|
|
180
|
+
historyMap: BncrGroupHistoryMap;
|
|
181
|
+
parsed: ParsedInbound;
|
|
182
|
+
}) {
|
|
183
|
+
const historyKey = buildBncrGroupHistoryKey(args.parsed);
|
|
184
|
+
if (!historyKey) return;
|
|
185
|
+
createChannelHistoryWindow({ historyMap: args.historyMap }).clear({
|
|
186
|
+
historyKey,
|
|
187
|
+
limit: DEFAULT_GROUP_HISTORY_LIMIT,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -53,12 +53,12 @@ export function buildNativeCommandHandledResult(args: {
|
|
|
53
53
|
|
|
54
54
|
export function buildBncrNativeCommandSessionState(args: {
|
|
55
55
|
parsed: ParsedInbound;
|
|
56
|
-
|
|
56
|
+
sessionAgentId: string;
|
|
57
57
|
resolvedRoute: { sessionKey: string };
|
|
58
58
|
}) {
|
|
59
|
-
const { parsed,
|
|
59
|
+
const { parsed, sessionAgentId, resolvedRoute } = args;
|
|
60
60
|
const baseSessionKey =
|
|
61
|
-
normalizeInboundSessionKey(parsed.sessionKeyfromroute, parsed.route,
|
|
61
|
+
normalizeInboundSessionKey(parsed.sessionKeyfromroute, parsed.route, sessionAgentId) ||
|
|
62
62
|
resolvedRoute.sessionKey;
|
|
63
63
|
const taskSessionKey = withTaskSessionKey(baseSessionKey, parsed.extracted.taskKey);
|
|
64
64
|
const sessionKey = taskSessionKey || baseSessionKey;
|
|
@@ -87,71 +87,87 @@ export function createNativeCommandTurnContext(args: {
|
|
|
87
87
|
senderDisplayName: string;
|
|
88
88
|
body: string;
|
|
89
89
|
}): BncrInboundContextPayload | Promise<BncrInboundContextPayload> {
|
|
90
|
-
return
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
id: args.peer.id,
|
|
106
|
-
label: args.displayTo,
|
|
107
|
-
routePeer: {
|
|
90
|
+
return Promise.resolve(
|
|
91
|
+
resolveBncrChannelInboundRuntime(args.api).buildContext({
|
|
92
|
+
channel: args.channelId,
|
|
93
|
+
provider: args.channelId,
|
|
94
|
+
surface: args.channelId,
|
|
95
|
+
accountId: args.accountId,
|
|
96
|
+
messageId: args.msgId,
|
|
97
|
+
timestamp: Date.now(),
|
|
98
|
+
from: args.senderIdForContext,
|
|
99
|
+
sender: {
|
|
100
|
+
id: args.senderIdForContext,
|
|
101
|
+
name: args.senderDisplayName,
|
|
102
|
+
username: args.senderDisplayName,
|
|
103
|
+
},
|
|
104
|
+
conversation: {
|
|
108
105
|
kind: args.peer.kind,
|
|
109
106
|
id: args.peer.id,
|
|
107
|
+
label: args.displayTo,
|
|
108
|
+
routePeer: {
|
|
109
|
+
kind: args.peer.kind,
|
|
110
|
+
id: args.peer.id,
|
|
111
|
+
},
|
|
110
112
|
},
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
113
|
+
route: {
|
|
114
|
+
agentId: args.resolvedRoute.agentId,
|
|
115
|
+
accountId: args.accountId,
|
|
116
|
+
routeSessionKey: args.resolvedRoute.sessionKey,
|
|
117
|
+
dispatchSessionKey: args.sessionKey,
|
|
118
|
+
mainSessionKey: args.resolvedRoute.mainSessionKey,
|
|
119
|
+
},
|
|
120
|
+
reply: {
|
|
121
|
+
to: args.displayTo,
|
|
122
|
+
originatingTo: args.originatingTo,
|
|
123
|
+
replyToId: args.msgId,
|
|
124
|
+
},
|
|
125
|
+
message: {
|
|
126
|
+
inboundEventKind: 'user_request',
|
|
127
|
+
body: args.body,
|
|
128
|
+
rawBody: args.body,
|
|
129
|
+
bodyForAgent: args.body,
|
|
130
|
+
commandBody: args.body,
|
|
131
|
+
envelopeFrom: args.originatingTo,
|
|
132
|
+
senderLabel: args.senderDisplayName,
|
|
133
|
+
},
|
|
134
|
+
commandTurn: {
|
|
135
|
+
kind: 'native',
|
|
136
|
+
source: 'native',
|
|
137
|
+
authorized: true,
|
|
138
|
+
body: args.body,
|
|
139
|
+
},
|
|
140
|
+
access: {
|
|
141
|
+
mentions: {
|
|
142
|
+
canDetectMention: true,
|
|
143
|
+
wasMentioned: true,
|
|
144
|
+
effectiveWasMentioned: true,
|
|
145
|
+
},
|
|
146
|
+
commands: {
|
|
147
|
+
authorized: true,
|
|
148
|
+
allowTextCommands: true,
|
|
149
|
+
useAccessGroups: false,
|
|
150
|
+
authorizers: [],
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
extra: {
|
|
154
|
+
OriginatingChannel: args.channelId,
|
|
155
|
+
},
|
|
156
|
+
}),
|
|
157
|
+
).then((ctx) => {
|
|
158
|
+
ctx.From = args.senderIdForContext;
|
|
159
|
+
ctx.To = args.displayTo;
|
|
160
|
+
ctx.SenderId = args.senderIdForContext;
|
|
161
|
+
ctx.OriginatingChannel = args.channelId;
|
|
162
|
+
ctx.CommandAuthorized = true;
|
|
163
|
+
ctx.CommandSource = 'native';
|
|
164
|
+
ctx.CommandTurn = {
|
|
134
165
|
kind: 'native',
|
|
135
166
|
source: 'native',
|
|
136
167
|
authorized: true,
|
|
137
168
|
body: args.body,
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
mentions: {
|
|
141
|
-
canDetectMention: true,
|
|
142
|
-
wasMentioned: true,
|
|
143
|
-
effectiveWasMentioned: true,
|
|
144
|
-
},
|
|
145
|
-
commands: {
|
|
146
|
-
authorized: true,
|
|
147
|
-
allowTextCommands: true,
|
|
148
|
-
useAccessGroups: false,
|
|
149
|
-
authorizers: [],
|
|
150
|
-
},
|
|
151
|
-
},
|
|
152
|
-
extra: {
|
|
153
|
-
OriginatingChannel: args.channelId,
|
|
154
|
-
},
|
|
169
|
+
};
|
|
170
|
+
return ctx;
|
|
155
171
|
});
|
|
156
172
|
}
|
|
157
173
|
|
|
@@ -2,6 +2,11 @@ export type NativeCommand = {
|
|
|
2
2
|
command: string;
|
|
3
3
|
raw: string;
|
|
4
4
|
body: string;
|
|
5
|
+
argsText: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type ParseBncrNativeCommandOptions = {
|
|
9
|
+
allowBareWhoami?: boolean;
|
|
5
10
|
};
|
|
6
11
|
|
|
7
12
|
export type NativeVerboseCommand = {
|
|
@@ -10,27 +15,106 @@ export type NativeVerboseCommand = {
|
|
|
10
15
|
text: string;
|
|
11
16
|
};
|
|
12
17
|
|
|
13
|
-
export
|
|
18
|
+
export type NativeHelpCommand = {
|
|
19
|
+
handled: true;
|
|
20
|
+
text: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export type NativeWhoamiCommand = {
|
|
24
|
+
handled: true;
|
|
25
|
+
text: string;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const BNCR_HELP_TEXT = [
|
|
29
|
+
'🦞 Bncr command usage',
|
|
30
|
+
'',
|
|
31
|
+
'📌 Bncr builtins',
|
|
32
|
+
' • /bncr whoami',
|
|
33
|
+
' • /bncr verbose on|off|full',
|
|
34
|
+
'',
|
|
35
|
+
'🛡 Scene approval',
|
|
36
|
+
' • /bncr allow [<platform>:<groupId>]',
|
|
37
|
+
' • /bncr deny [<platform>:<groupId>]',
|
|
38
|
+
' • /bncr bind <agentId> [<platform>:<groupId>]',
|
|
39
|
+
' • /bncr mode',
|
|
40
|
+
' • /bncr mode help',
|
|
41
|
+
' • /bncr mode <admin|mention|hybrid|all> [<platform>:<groupId>]',
|
|
42
|
+
' • /bncr revoke [<platform>:<groupId>]',
|
|
43
|
+
' • /bncr list pending',
|
|
44
|
+
' • /bncr list scenes',
|
|
45
|
+
].join('\n');
|
|
46
|
+
|
|
47
|
+
const BNCR_NATIVE_COMMANDS = new Set([
|
|
48
|
+
'help',
|
|
49
|
+
'whoami',
|
|
50
|
+
'verbose',
|
|
51
|
+
'allow',
|
|
52
|
+
'deny',
|
|
53
|
+
'bind',
|
|
54
|
+
'mode',
|
|
55
|
+
'revoke',
|
|
56
|
+
'list',
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
export function parseBncrNativeCommand(
|
|
60
|
+
text: string,
|
|
61
|
+
options?: ParseBncrNativeCommandOptions,
|
|
62
|
+
): NativeCommand | null {
|
|
14
63
|
const raw = String(text || '').trim();
|
|
15
|
-
|
|
16
|
-
|
|
64
|
+
const allowBareWhoami = options?.allowBareWhoami !== false;
|
|
65
|
+
if (allowBareWhoami && raw.toLowerCase() === '/whoami') {
|
|
66
|
+
return { command: 'whoami', raw, body: '/whoami', argsText: '' };
|
|
67
|
+
}
|
|
68
|
+
if (!raw.startsWith('/bncr')) return null;
|
|
69
|
+
const match = raw.match(/^\/bncr(?:@[A-Za-z0-9_]+)?(?:\s+([^\s]+)(?:\s+([\s\S]*))?)?$/i);
|
|
17
70
|
if (!match) return null;
|
|
18
71
|
|
|
19
|
-
const command = String(match[1] || '')
|
|
72
|
+
const command = String(match[1] || 'help')
|
|
20
73
|
.trim()
|
|
21
74
|
.toLowerCase();
|
|
22
75
|
if (!command) return null;
|
|
76
|
+
if (!BNCR_NATIVE_COMMANDS.has(command)) return null;
|
|
23
77
|
|
|
24
|
-
const
|
|
25
|
-
const body =
|
|
26
|
-
|
|
78
|
+
const argsText = String(match[2] || '').trim();
|
|
79
|
+
const body =
|
|
80
|
+
command === 'help' ? '/commands' : [`/${command}`, argsText].filter(Boolean).join(' ');
|
|
81
|
+
return { command, raw, body, argsText };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function resolveBncrNativeHelpCommand(command: NativeCommand): NativeHelpCommand | null {
|
|
85
|
+
if (command.command !== 'help') return null;
|
|
86
|
+
return { handled: true, text: BNCR_HELP_TEXT };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function resolveBncrNativeWhoamiCommand(args: {
|
|
90
|
+
command: NativeCommand;
|
|
91
|
+
platform: string;
|
|
92
|
+
groupId: string;
|
|
93
|
+
groupName?: string;
|
|
94
|
+
userId: string;
|
|
95
|
+
userName?: string;
|
|
96
|
+
isGroup: boolean;
|
|
97
|
+
isAdmin: boolean;
|
|
98
|
+
}): NativeWhoamiCommand | null {
|
|
99
|
+
if (args.command.command !== 'whoami') return null;
|
|
100
|
+
const lines = ['🧭 Bncr Identity', ''];
|
|
101
|
+
lines.push(`Platform: ${args.platform || '(unknown)'}`);
|
|
102
|
+
lines.push(`User: ${args.userName || '(unknown)'} (${args.userId || '0'})`);
|
|
103
|
+
if (args.isGroup) {
|
|
104
|
+
lines.push(`Group: ${args.groupName || '(unknown)'} (${args.groupId || '0'})`);
|
|
105
|
+
lines.push(`Scene: ${args.platform || '(unknown)'}:${args.groupId || '0'}`);
|
|
106
|
+
} else {
|
|
107
|
+
lines.push(`Scene: ${args.platform || '(unknown)'}:${args.userId || '0'}`);
|
|
108
|
+
}
|
|
109
|
+
lines.push(`Admin: ${args.isAdmin ? 'true' : 'false'}`);
|
|
110
|
+
return { handled: true, text: lines.join('\n') };
|
|
27
111
|
}
|
|
28
112
|
|
|
29
113
|
export function resolveBncrNativeVerboseCommand(
|
|
30
114
|
command: NativeCommand,
|
|
31
115
|
): NativeVerboseCommand | null {
|
|
32
116
|
if (command.command !== 'verbose') return null;
|
|
33
|
-
const rawLevel = String(command.
|
|
117
|
+
const rawLevel = String(command.argsText || '')
|
|
34
118
|
.trim()
|
|
35
119
|
.toLowerCase();
|
|
36
120
|
if (!rawLevel || rawLevel === 'status') {
|
|
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { normalizeAccountId } from '../../core/accounts.ts';
|
|
3
3
|
import { extractInlineTaskKey } from '../../core/targets.ts';
|
|
4
4
|
import type { BncrRoute } from '../../core/types.ts';
|
|
5
|
-
import type { BncrInboundParamsInput } from './contracts.ts';
|
|
5
|
+
import type { BncrInboundMediaItem, BncrInboundParamsInput } from './contracts.ts';
|
|
6
6
|
|
|
7
7
|
function asString(v: unknown, fallback = ''): string {
|
|
8
8
|
if (typeof v === 'string') return v;
|
|
@@ -10,6 +10,84 @@ function asString(v: unknown, fallback = ''): string {
|
|
|
10
10
|
return String(v);
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
function asBoolean(v: unknown, fallback = false): boolean {
|
|
14
|
+
if (typeof v === 'boolean') return v;
|
|
15
|
+
if (typeof v === 'number') return v !== 0;
|
|
16
|
+
if (typeof v === 'string') {
|
|
17
|
+
const raw = v.trim().toLowerCase();
|
|
18
|
+
if (!raw) return fallback;
|
|
19
|
+
if (['true', '1', 'yes', 'y', 'on'].includes(raw)) return true;
|
|
20
|
+
if (['false', '0', 'no', 'n', 'off'].includes(raw)) return false;
|
|
21
|
+
}
|
|
22
|
+
return fallback;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function asStringArray(v: unknown): string[] {
|
|
26
|
+
if (Array.isArray(v)) {
|
|
27
|
+
return v.map((item) => asString(item).trim()).filter(Boolean);
|
|
28
|
+
}
|
|
29
|
+
if (typeof v === 'string') {
|
|
30
|
+
return v
|
|
31
|
+
.split(',')
|
|
32
|
+
.map((item) => item.trim())
|
|
33
|
+
.filter(Boolean);
|
|
34
|
+
}
|
|
35
|
+
return [];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function asInboundMediaItems(params: BncrInboundParamsInput): BncrInboundMediaItem[] {
|
|
39
|
+
const normalized: BncrInboundMediaItem[] = [];
|
|
40
|
+
const rawList = Array.isArray(params?.mediaList) ? params.mediaList : [];
|
|
41
|
+
for (const item of rawList) {
|
|
42
|
+
if (!item || typeof item !== 'object') continue;
|
|
43
|
+
const path = asString((item as { path?: unknown }).path || '').trim();
|
|
44
|
+
const base64 = asString((item as { base64?: unknown }).base64 || '').trim();
|
|
45
|
+
const mimeType = asString((item as { mimeType?: unknown }).mimeType || '').trim();
|
|
46
|
+
const fileName = asString((item as { fileName?: unknown }).fileName || '').trim();
|
|
47
|
+
const type = asString((item as { type?: unknown }).type || '').trim();
|
|
48
|
+
const transferId = asString((item as { transferId?: unknown }).transferId || '').trim();
|
|
49
|
+
if (!path && !base64) continue;
|
|
50
|
+
normalized.push({
|
|
51
|
+
...(path ? { path } : {}),
|
|
52
|
+
...(base64 ? { base64 } : {}),
|
|
53
|
+
...(mimeType ? { mimeType } : {}),
|
|
54
|
+
...(fileName ? { fileName } : {}),
|
|
55
|
+
...(type ? { type } : {}),
|
|
56
|
+
...(transferId ? { transferId } : {}),
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (normalized.length > 0) return normalized;
|
|
61
|
+
|
|
62
|
+
const legacyPath = asString(params?.path || '').trim();
|
|
63
|
+
const legacyBase64 = asString(params?.base64 || '').trim();
|
|
64
|
+
const legacyMimeType = asString(params?.mimeType || '').trim();
|
|
65
|
+
const legacyFileName = asString(params?.fileName || '').trim();
|
|
66
|
+
const legacyType = asString(params?.type || '').trim();
|
|
67
|
+
const legacyTransferId = asString((params as { transferId?: unknown })?.transferId || '').trim();
|
|
68
|
+
if (legacyPath || legacyBase64) {
|
|
69
|
+
return [
|
|
70
|
+
{
|
|
71
|
+
...(legacyPath ? { path: legacyPath } : {}),
|
|
72
|
+
...(legacyBase64 ? { base64: legacyBase64 } : {}),
|
|
73
|
+
...(legacyMimeType ? { mimeType: legacyMimeType } : {}),
|
|
74
|
+
...(legacyFileName ? { fileName: legacyFileName } : {}),
|
|
75
|
+
...(legacyType ? { type: legacyType } : {}),
|
|
76
|
+
...(legacyTransferId ? { transferId: legacyTransferId } : {}),
|
|
77
|
+
},
|
|
78
|
+
];
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const legacyPaths = asStringArray(params?.paths);
|
|
82
|
+
return legacyPaths.map((item) => ({
|
|
83
|
+
path: item,
|
|
84
|
+
...(legacyMimeType ? { mimeType: legacyMimeType } : {}),
|
|
85
|
+
...(legacyFileName ? { fileName: legacyFileName } : {}),
|
|
86
|
+
...(legacyType ? { type: legacyType } : {}),
|
|
87
|
+
...(legacyTransferId ? { transferId: legacyTransferId } : {}),
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
|
|
13
91
|
export function inboundDedupKey(params: {
|
|
14
92
|
accountId: string;
|
|
15
93
|
platform: string;
|
|
@@ -36,24 +114,30 @@ export function inboundDedupKey(params: {
|
|
|
36
114
|
return `${accountId}|${platform}|${groupId}|${userId}|hash:${digest}`;
|
|
37
115
|
}
|
|
38
116
|
|
|
39
|
-
export function resolveChatType(
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
// group semantics without updating session routing, reply target policy, and requireMention
|
|
43
|
-
// behavior together.
|
|
117
|
+
export function resolveChatType(route: BncrRoute, isGroup: boolean): 'direct' | 'group' {
|
|
118
|
+
if (isGroup) return 'group';
|
|
119
|
+
if (route.groupId !== '0') return 'group';
|
|
44
120
|
return 'direct';
|
|
45
121
|
}
|
|
46
122
|
|
|
47
123
|
export function parseBncrInboundParams(params: BncrInboundParamsInput) {
|
|
48
124
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
125
|
+
const protocolVersion = asString(params?.protocolVersion || '').trim() || undefined;
|
|
126
|
+
const capabilities = asStringArray(params?.capabilities);
|
|
49
127
|
const platform = asString(params?.platform || '').trim();
|
|
50
128
|
const groupId = asString(params?.groupId || '0').trim() || '0';
|
|
129
|
+
const groupName = asString(params?.groupName || '').trim();
|
|
51
130
|
const userId = asString(params?.userId || '').trim();
|
|
131
|
+
const userName = asString(params?.userName || '').trim();
|
|
52
132
|
const sessionKeyfromroute = asString(params?.sessionKey || '').trim();
|
|
53
133
|
const providedOriginatingTo =
|
|
54
134
|
asString(params?.originatingTo || params?.providedOriginatingTo || params?.to || '').trim() ||
|
|
55
135
|
undefined;
|
|
56
136
|
const clientId = asString(params?.clientId || '').trim() || undefined;
|
|
137
|
+
const bridgeId = asString(params?.bridgeId || params?.clientId || '').trim() || undefined;
|
|
138
|
+
const bridgeName = asString(params?.bridgeName || 'Bncr').trim() || 'Bncr';
|
|
139
|
+
const isGroup = asBoolean(params?.isGroup, groupId !== '0');
|
|
140
|
+
const isAdmin = asBoolean(params?.isAdmin, false);
|
|
57
141
|
|
|
58
142
|
const route: BncrRoute = {
|
|
59
143
|
platform,
|
|
@@ -63,11 +147,17 @@ export function parseBncrInboundParams(params: BncrInboundParamsInput) {
|
|
|
63
147
|
|
|
64
148
|
const text = asString(params?.msg || '');
|
|
65
149
|
const msgType = asString(params?.type || 'text') || 'text';
|
|
150
|
+
const mediaItems = asInboundMediaItems(params);
|
|
66
151
|
const mediaBase64 = asString(params?.base64 || '');
|
|
67
152
|
const mediaPathFromTransfer = asString(params?.path || '').trim();
|
|
68
153
|
const mimeType = asString(params?.mimeType || '').trim() || undefined;
|
|
69
154
|
const fileName = asString(params?.fileName || '').trim() || undefined;
|
|
70
155
|
const msgId = asString(params?.msgId || '').trim() || undefined;
|
|
156
|
+
const shouldRespond = asBoolean(params?.shouldRespond, false);
|
|
157
|
+
const triggerKind = asString(params?.triggerKind || 'none').trim() || 'none';
|
|
158
|
+
const botName = asString(params?.botName || '').trim();
|
|
159
|
+
const isBotMentioned = asBoolean(params?.isBotMentioned, false);
|
|
160
|
+
const isReplyToBot = asBoolean(params?.isReplyToBot, false);
|
|
71
161
|
|
|
72
162
|
const dedupKey = inboundDedupKey({
|
|
73
163
|
accountId,
|
|
@@ -79,29 +169,44 @@ export function parseBncrInboundParams(params: BncrInboundParamsInput) {
|
|
|
79
169
|
mediaBase64,
|
|
80
170
|
});
|
|
81
171
|
|
|
172
|
+
const peerKind = resolveChatType(route, isGroup);
|
|
82
173
|
const peer = {
|
|
83
|
-
kind:
|
|
84
|
-
id:
|
|
174
|
+
kind: peerKind,
|
|
175
|
+
id: peerKind === 'group' ? route.groupId : route.userId,
|
|
85
176
|
} as const;
|
|
86
177
|
|
|
87
178
|
const extracted = extractInlineTaskKey(text);
|
|
88
179
|
|
|
89
180
|
return {
|
|
90
181
|
accountId,
|
|
182
|
+
protocolVersion,
|
|
183
|
+
capabilities,
|
|
91
184
|
platform,
|
|
92
185
|
groupId,
|
|
186
|
+
groupName,
|
|
93
187
|
userId,
|
|
188
|
+
userName,
|
|
94
189
|
sessionKeyfromroute,
|
|
95
190
|
providedOriginatingTo,
|
|
96
191
|
clientId,
|
|
192
|
+
bridgeId,
|
|
193
|
+
bridgeName,
|
|
194
|
+
isGroup,
|
|
195
|
+
isAdmin,
|
|
97
196
|
route,
|
|
98
197
|
text,
|
|
99
198
|
msgType,
|
|
199
|
+
mediaItems,
|
|
100
200
|
mediaBase64,
|
|
101
201
|
mediaPathFromTransfer,
|
|
102
202
|
mimeType,
|
|
103
203
|
fileName,
|
|
104
204
|
msgId,
|
|
205
|
+
shouldRespond,
|
|
206
|
+
triggerKind,
|
|
207
|
+
botName,
|
|
208
|
+
isBotMentioned,
|
|
209
|
+
isReplyToBot,
|
|
105
210
|
dedupKey,
|
|
106
211
|
peer,
|
|
107
212
|
extracted,
|