@xmoxmo/bncr 0.5.5 → 0.5.6
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 +48 -7
- package/src/core/outbox-entry-builders.ts +7 -0
- package/src/messaging/inbound/native-command.ts +7 -3
- package/src/messaging/inbound/scene-admin.ts +181 -5
- package/src/messaging/outbound/marker-parser.ts +154 -0
- package/src/messaging/outbound/media.ts +5 -14
- package/src/messaging/outbound/reply-enqueue-media.ts +4 -0
- package/src/messaging/outbound/reply-enqueue.ts +25 -3
- package/src/messaging/outbound/send-params.ts +43 -10
- package/src/messaging/outbound/send.ts +6 -0
- package/src/plugin/bridge-file-transfer-push-facade.ts +3 -1
- package/src/plugin/bridge-media-facade.ts +2 -1
- package/src/plugin/channel-plugin-surface-group.ts +30 -0
- package/src/plugin/channel-runtime-builders-delivery.ts +2 -0
- package/src/plugin/channel-runtime-types.ts +8 -0
- package/src/plugin/channel-send.ts +197 -94
- package/src/plugin/file-transfer-orchestrator.ts +105 -3
- package/src/plugin/file-transfer-setup.ts +1 -1
- package/src/plugin/media-orchestrators-runtime-group.ts +2 -0
- package/src/plugin/outbound.ts +4 -0
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -288,6 +288,9 @@ class BncrBridgeRuntime {
|
|
|
288
288
|
{ accountId: string; route: BncrRoute; updatedAt: number }
|
|
289
289
|
>();
|
|
290
290
|
private sceneRegistry = new Map<string, BncrSceneRecord>();
|
|
291
|
+
getSceneRegistry(): Map<string, BncrSceneRecord> {
|
|
292
|
+
return this.sceneRegistry;
|
|
293
|
+
}
|
|
291
294
|
private groupHistories: BncrGroupHistoryMap = new Map();
|
|
292
295
|
private routeAliases = new Map<
|
|
293
296
|
string,
|
|
@@ -1186,6 +1189,7 @@ class BncrBridgeRuntime {
|
|
|
1186
1189
|
kind?: 'tool' | 'block' | 'final';
|
|
1187
1190
|
replyToId?: string;
|
|
1188
1191
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
1192
|
+
downloadMedia?: boolean;
|
|
1189
1193
|
}): OutboxEntry {
|
|
1190
1194
|
return buildFileTransferOutboxEntryFromRuntime({
|
|
1191
1195
|
createMessageId: () => randomUUID(),
|
|
@@ -1205,6 +1209,7 @@ class BncrBridgeRuntime {
|
|
|
1205
1209
|
kind: params.kind,
|
|
1206
1210
|
replyToId: asString(params.replyToId || '').trim() || undefined,
|
|
1207
1211
|
replyTargetPolicy: params.replyTargetPolicy,
|
|
1212
|
+
downloadMedia: params.downloadMedia === true ? true : undefined,
|
|
1208
1213
|
});
|
|
1209
1214
|
}
|
|
1210
1215
|
|
|
@@ -1222,6 +1227,8 @@ class BncrBridgeRuntime {
|
|
|
1222
1227
|
this.mediaDedupeRuntime.rememberRecentMediaSend(params);
|
|
1223
1228
|
}
|
|
1224
1229
|
|
|
1230
|
+
private HTTP_URL_RE = /^https?:\/\//i;
|
|
1231
|
+
|
|
1225
1232
|
private tryBuildMediaDedupeFallback(params: {
|
|
1226
1233
|
sessionKey: string;
|
|
1227
1234
|
mediaUrl: string;
|
|
@@ -1229,6 +1236,8 @@ class BncrBridgeRuntime {
|
|
|
1229
1236
|
replyToId: string;
|
|
1230
1237
|
currentTime?: number;
|
|
1231
1238
|
}): { text: string; reason: 'same-text-sent-checkmark' | 'text-changed-downgrade' } | null {
|
|
1239
|
+
// Only deduplicate local file paths - HTTP URLs bypass dedup
|
|
1240
|
+
if (this.HTTP_URL_RE.test(params.mediaUrl)) return null;
|
|
1232
1241
|
return this.mediaDedupeRuntime.tryBuildMediaDedupeFallback(params);
|
|
1233
1242
|
}
|
|
1234
1243
|
|
|
@@ -1246,6 +1255,17 @@ class BncrBridgeRuntime {
|
|
|
1246
1255
|
? params.meta.messageKind
|
|
1247
1256
|
: undefined;
|
|
1248
1257
|
|
|
1258
|
+
const hasPayload = !!(params.media.base64 || params.media.path);
|
|
1259
|
+
if (hasPayload) {
|
|
1260
|
+
console.log(
|
|
1261
|
+
'[bncr] outbound-media download=true|media=' +
|
|
1262
|
+
(params.media.path || `base64:${(params.media.base64 || '').substring(0, 12)}...`) +
|
|
1263
|
+
'|url=' +
|
|
1264
|
+
params.mediaUrl.split('?')[0],
|
|
1265
|
+
);
|
|
1266
|
+
} else {
|
|
1267
|
+
console.log(`[bncr] outbound-media pass-through|url=${params.mediaUrl.split('?')[0]}`);
|
|
1268
|
+
}
|
|
1249
1269
|
return buildBncrMediaOutboundFrame({
|
|
1250
1270
|
messageId: params.entry.messageId,
|
|
1251
1271
|
sessionKey: params.entry.sessionKey,
|
|
@@ -1254,7 +1274,7 @@ class BncrBridgeRuntime {
|
|
|
1254
1274
|
mode: params.media.path ? 'chunk' : 'base64',
|
|
1255
1275
|
mimeType: params.media.mimeType,
|
|
1256
1276
|
fileName: params.media.fileName,
|
|
1257
|
-
|
|
1277
|
+
base64: params.media.base64,
|
|
1258
1278
|
path: params.media.path,
|
|
1259
1279
|
},
|
|
1260
1280
|
mediaUrl: params.mediaUrl,
|
|
@@ -1281,9 +1301,11 @@ class BncrBridgeRuntime {
|
|
|
1281
1301
|
sessionKey: string;
|
|
1282
1302
|
route: BncrRoute;
|
|
1283
1303
|
text: string;
|
|
1304
|
+
extra?: Record<string, unknown>;
|
|
1284
1305
|
kind?: 'tool' | 'block' | 'final';
|
|
1285
1306
|
replyToId?: string;
|
|
1286
1307
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
1308
|
+
downloadMedia?: boolean;
|
|
1287
1309
|
}): OutboxEntry {
|
|
1288
1310
|
return buildTextOutboxEntryFromRuntime({
|
|
1289
1311
|
createMessageId: () => randomUUID(),
|
|
@@ -1294,9 +1316,11 @@ class BncrBridgeRuntime {
|
|
|
1294
1316
|
sessionKey: params.sessionKey,
|
|
1295
1317
|
route: params.route,
|
|
1296
1318
|
text: params.text,
|
|
1319
|
+
extra: params.extra && Object.keys(params.extra).length > 0 ? { ...params.extra } : undefined,
|
|
1297
1320
|
kind: params.kind,
|
|
1298
1321
|
replyToId: params.replyToId,
|
|
1299
1322
|
replyTargetPolicy: params.replyTargetPolicy,
|
|
1323
|
+
downloadMedia: params.downloadMedia === true ? true : undefined,
|
|
1300
1324
|
});
|
|
1301
1325
|
}
|
|
1302
1326
|
|
|
@@ -2722,7 +2746,7 @@ class BncrBridgeRuntime {
|
|
|
2722
2746
|
mode: 'base64' | 'chunk';
|
|
2723
2747
|
mimeType?: string;
|
|
2724
2748
|
fileName?: string;
|
|
2725
|
-
|
|
2749
|
+
base64?: string;
|
|
2726
2750
|
path?: string;
|
|
2727
2751
|
}> {
|
|
2728
2752
|
return this.bridgeMediaFacade.transferMediaToBncrClient(params);
|
|
@@ -2985,6 +3009,22 @@ class BncrBridgeRuntime {
|
|
|
2985
3009
|
await stopBncrStatusWorker(this.buildStatusWorkerRuntime(), ctx);
|
|
2986
3010
|
};
|
|
2987
3011
|
|
|
3012
|
+
/**
|
|
3013
|
+
* Merge host-level outbound fields (forceDocument, gifPlayback, silent)
|
|
3014
|
+
* into extra before dispatching to channel-send runtime.
|
|
3015
|
+
*/
|
|
3016
|
+
private mergeHostFields(ctx: BncrChannelSendContext): BncrChannelSendContext {
|
|
3017
|
+
if (!ctx.forceDocument && !ctx.gifPlayback && !ctx.silent) {
|
|
3018
|
+
return ctx;
|
|
3019
|
+
}
|
|
3020
|
+
const extra = { ...(typeof ctx.extra === 'object' && ctx.extra !== null ? ctx.extra : {}) };
|
|
3021
|
+
if (ctx.forceDocument === true) extra.forceDocument = true;
|
|
3022
|
+
if (ctx.gifPlayback === true) extra.gifPlayback = true;
|
|
3023
|
+
if (ctx.silent === true) extra.silent = true;
|
|
3024
|
+
// Clear original fields so resolveUnifiedOutboundExtra doesn't re-add and override marker values
|
|
3025
|
+
return { ...ctx, extra, forceDocument: undefined, gifPlayback: undefined, silent: undefined };
|
|
3026
|
+
}
|
|
3027
|
+
|
|
2988
3028
|
// Channel send surface assembly ------------------------------------------
|
|
2989
3029
|
// This group backs the public channel send APIs and should stay adjacent to
|
|
2990
3030
|
// the final exposed channel* methods for quick top-down scanning.
|
|
@@ -2996,19 +3036,19 @@ class BncrBridgeRuntime {
|
|
|
2996
3036
|
private readonly channelSendRuntime = this.channelSendRuntimeGroup.channelSendRuntime;
|
|
2997
3037
|
|
|
2998
3038
|
channelSendText = async (ctx: BncrChannelSendContext) =>
|
|
2999
|
-
this.channelSendRuntime.channelSendText(ctx);
|
|
3039
|
+
this.channelSendRuntime.channelSendText(this.mergeHostFields(ctx));
|
|
3000
3040
|
|
|
3001
3041
|
channelSendMedia = async (ctx: BncrChannelSendContext) =>
|
|
3002
|
-
this.channelSendRuntime.channelSendMedia(ctx);
|
|
3042
|
+
this.channelSendRuntime.channelSendMedia(this.mergeHostFields(ctx));
|
|
3003
3043
|
|
|
3004
3044
|
channelMessageSendText = async (ctx: BncrChannelSendContext) =>
|
|
3005
|
-
this.channelSendRuntime.channelMessageSendText(ctx);
|
|
3045
|
+
this.channelSendRuntime.channelMessageSendText(this.mergeHostFields(ctx));
|
|
3006
3046
|
|
|
3007
3047
|
channelMessageSendMedia = async (ctx: BncrChannelSendContext) =>
|
|
3008
|
-
this.channelSendRuntime.channelMessageSendMedia(ctx);
|
|
3048
|
+
this.channelSendRuntime.channelMessageSendMedia(this.mergeHostFields(ctx));
|
|
3009
3049
|
|
|
3010
3050
|
channelMessageSendPayload = async (ctx: BncrChannelSendContext) =>
|
|
3011
|
-
this.channelSendRuntime.channelMessageSendPayload(ctx);
|
|
3051
|
+
this.channelSendRuntime.channelMessageSendPayload(this.mergeHostFields(ctx));
|
|
3012
3052
|
}
|
|
3013
3053
|
|
|
3014
3054
|
// Plugin surface export -----------------------------------------------------
|
|
@@ -3029,6 +3069,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
3029
3069
|
|
|
3030
3070
|
return createBncrChannelPluginSurfaceGroup({
|
|
3031
3071
|
channelId: CHANNEL_ID,
|
|
3072
|
+
sceneRegistry: getBridge().getSceneRegistry(),
|
|
3032
3073
|
getMessageSendBridge: bridgeGroup.getMessageSendBridge,
|
|
3033
3074
|
getOutboundBridge: bridgeGroup.getOutboundBridge,
|
|
3034
3075
|
getMessagingBridge: bridgeGroup.getMessagingBridge,
|
|
@@ -20,6 +20,7 @@ export function buildFileTransferOutboxEntry(args: {
|
|
|
20
20
|
kind?: 'tool' | 'block' | 'final';
|
|
21
21
|
replyToId?: string;
|
|
22
22
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
23
|
+
downloadMedia?: boolean;
|
|
23
24
|
}): OutboxEntry {
|
|
24
25
|
const messageId = args.createMessageId();
|
|
25
26
|
const createdAt = args.now();
|
|
@@ -39,6 +40,7 @@ export function buildFileTransferOutboxEntry(args: {
|
|
|
39
40
|
asVoice: args.asVoice === true,
|
|
40
41
|
audioAsVoice: args.audioAsVoice === true,
|
|
41
42
|
type: args.type,
|
|
43
|
+
downloadMedia: args.downloadMedia === true,
|
|
42
44
|
...(args.extra ? { extra: { ...args.extra } } : {}),
|
|
43
45
|
finalEvent: args.pushEvent,
|
|
44
46
|
replyToId:
|
|
@@ -62,12 +64,14 @@ export function buildTextOutboxEntry(args: {
|
|
|
62
64
|
normalizeAccountId: (accountId?: string | null) => string;
|
|
63
65
|
normalizeReplyToId: (value?: string | null) => string;
|
|
64
66
|
accountId: string;
|
|
67
|
+
extra?: Record<string, unknown>;
|
|
65
68
|
sessionKey: string;
|
|
66
69
|
route: BncrRoute;
|
|
67
70
|
text: string;
|
|
68
71
|
kind?: 'tool' | 'block' | 'final';
|
|
69
72
|
replyToId?: string;
|
|
70
73
|
replyTargetPolicy?: OutboundReplyTargetPolicy;
|
|
74
|
+
downloadMedia?: boolean;
|
|
71
75
|
}): OutboxEntry {
|
|
72
76
|
const messageId = args.createMessageId();
|
|
73
77
|
const createdAt = args.now();
|
|
@@ -92,6 +96,9 @@ export function buildTextOutboxEntry(args: {
|
|
|
92
96
|
path: '',
|
|
93
97
|
base64: '',
|
|
94
98
|
fileName: '',
|
|
99
|
+
// Extra fields from marker/pipeline spread for adapter-specific data
|
|
100
|
+
// (e.g. type="appmsg", msg="<appmsg>..." from [BncrParam:...])
|
|
101
|
+
...(args.extra ? { ...args.extra } : {}),
|
|
95
102
|
},
|
|
96
103
|
ts: createdAt,
|
|
97
104
|
};
|
|
@@ -50,15 +50,18 @@ const BNCR_HELP_TEXT = [
|
|
|
50
50
|
' • /bncr deny [<SceneId>]',
|
|
51
51
|
' • /bncr bind <agentId> [<SceneId>]',
|
|
52
52
|
' • /bncr mode help',
|
|
53
|
-
' • /bncr mode <admin|mention|hybrid|all> [<SceneId>]',
|
|
53
|
+
' • /bncr mode <admin|mention|hybrid|all|clear> [<SceneId>]',
|
|
54
54
|
' • /bncr revoke [<SceneId>]',
|
|
55
55
|
' • /bncr list pending [filters...]',
|
|
56
56
|
' • /bncr list scenes [filters...]',
|
|
57
57
|
'',
|
|
58
58
|
'📋 Group history',
|
|
59
59
|
' • /bncr history-help',
|
|
60
|
-
' • /bncr history-limit [<number
|
|
61
|
-
' • /bncr history-force on|off [<SceneId>]',
|
|
60
|
+
' • /bncr history-limit [<number>|clear] [<SceneId>]',
|
|
61
|
+
' • /bncr history-force on|off|clear [<SceneId>]',
|
|
62
|
+
'',
|
|
63
|
+
'🌐 Remote media',
|
|
64
|
+
' • /bncr download-media on|off|clear|default on|off [<SceneId>]',
|
|
62
65
|
].join('\n');
|
|
63
66
|
|
|
64
67
|
const BNCR_NATIVE_COMMANDS = new Set([
|
|
@@ -77,6 +80,7 @@ const BNCR_NATIVE_COMMANDS = new Set([
|
|
|
77
80
|
'history-limit',
|
|
78
81
|
'history-help',
|
|
79
82
|
'history-force',
|
|
83
|
+
'download-media',
|
|
80
84
|
]);
|
|
81
85
|
|
|
82
86
|
export const BNCR_DIRECT_ALLOWED_BARE_COMMANDS = new Set([
|
|
@@ -13,16 +13,22 @@ export type BncrSceneAdminCommand =
|
|
|
13
13
|
| { kind: 'bind'; sceneKey?: string; agentId: string }
|
|
14
14
|
| { kind: 'mode-help' }
|
|
15
15
|
| { kind: 'mode-get'; sceneKey?: string }
|
|
16
|
-
| { kind: 'mode'; sceneKey: string; mode:
|
|
16
|
+
| { kind: 'mode'; sceneKey: string; mode: string }
|
|
17
17
|
| { kind: 'list'; scope: 'pending' | 'scenes'; filters?: string[] }
|
|
18
18
|
| { kind: 'history-limit-get'; sceneKey?: string }
|
|
19
|
-
| { kind: 'history-limit-set'; sceneKey: string; limit: number }
|
|
19
|
+
| { kind: 'history-limit-set'; sceneKey: string; limit: number | 'clear' }
|
|
20
20
|
| { kind: 'history-force-get'; sceneKey?: string }
|
|
21
|
-
| { kind: 'history-force-set'; sceneKey: string; enabled: boolean }
|
|
22
|
-
| { kind: 'history-help' }
|
|
21
|
+
| { kind: 'history-force-set'; sceneKey: string; enabled: boolean | 'clear' }
|
|
22
|
+
| { kind: 'history-help' }
|
|
23
|
+
| { kind: 'download-media-get'; sceneKey?: string }
|
|
24
|
+
| { kind: 'download-media-set'; sceneKey: string; enabled: boolean }
|
|
25
|
+
| { kind: 'download-media-global-get' }
|
|
26
|
+
| { kind: 'download-media-global-set'; enabled: boolean };
|
|
23
27
|
|
|
24
28
|
const GROUP_REPLY_MODES = new Set<BncrGroupReplyMode>(['admin', 'mention', 'hybrid', 'all']);
|
|
25
29
|
|
|
30
|
+
const GLOBAL_SCENE_KEY = '__global__';
|
|
31
|
+
|
|
26
32
|
const MODE_HELP_TEXT = [
|
|
27
33
|
'💬 Bncr Group Reply Mode Configuration',
|
|
28
34
|
'',
|
|
@@ -122,6 +128,13 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
122
128
|
return { matched: true, valid: true, command: { kind: 'mode-get' } };
|
|
123
129
|
}
|
|
124
130
|
if (args.length === 1) {
|
|
131
|
+
if (args[0] === 'clear') {
|
|
132
|
+
return {
|
|
133
|
+
matched: true,
|
|
134
|
+
valid: true,
|
|
135
|
+
command: { kind: 'mode', sceneKey: '', mode: 'clear' as any },
|
|
136
|
+
};
|
|
137
|
+
}
|
|
125
138
|
if (args[0] === 'help') {
|
|
126
139
|
return {
|
|
127
140
|
matched: true,
|
|
@@ -142,6 +155,13 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
142
155
|
command: { kind: 'mode-get', sceneKey: args[0] },
|
|
143
156
|
};
|
|
144
157
|
}
|
|
158
|
+
if (args[0] === 'clear' && args[1]) {
|
|
159
|
+
return {
|
|
160
|
+
matched: true,
|
|
161
|
+
valid: true,
|
|
162
|
+
command: { kind: 'mode', sceneKey: args[1], mode: 'clear' as any },
|
|
163
|
+
};
|
|
164
|
+
}
|
|
145
165
|
if (GROUP_REPLY_MODES.has(args[0] as BncrGroupReplyMode) && args[1]) {
|
|
146
166
|
return {
|
|
147
167
|
matched: true,
|
|
@@ -181,6 +201,13 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
181
201
|
return { matched: true, valid: true, command: { kind: 'history-limit-get' } };
|
|
182
202
|
}
|
|
183
203
|
if (args.length === 1) {
|
|
204
|
+
if (args[0] === 'clear') {
|
|
205
|
+
return {
|
|
206
|
+
matched: true,
|
|
207
|
+
valid: true,
|
|
208
|
+
command: { kind: 'history-limit-set', sceneKey: '', limit: 'clear' as any },
|
|
209
|
+
};
|
|
210
|
+
}
|
|
184
211
|
const num = parseInt(args[0], 10);
|
|
185
212
|
if (Number.isFinite(num)) {
|
|
186
213
|
return {
|
|
@@ -196,6 +223,13 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
196
223
|
};
|
|
197
224
|
}
|
|
198
225
|
if (args.length === 2) {
|
|
226
|
+
if (args[0] === 'clear') {
|
|
227
|
+
return {
|
|
228
|
+
matched: true,
|
|
229
|
+
valid: true,
|
|
230
|
+
command: { kind: 'history-limit-set', sceneKey: args[1], limit: 'clear' as any },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
199
233
|
const num = parseInt(args[0], 10);
|
|
200
234
|
if (Number.isFinite(num)) {
|
|
201
235
|
return {
|
|
@@ -214,6 +248,20 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
214
248
|
if (args.length === 0) {
|
|
215
249
|
return { matched: true, valid: true, command: { kind: 'history-force-get' } };
|
|
216
250
|
}
|
|
251
|
+
if (args[0] === 'clear') {
|
|
252
|
+
if (args.length === 1) {
|
|
253
|
+
return {
|
|
254
|
+
matched: true,
|
|
255
|
+
valid: true,
|
|
256
|
+
command: { kind: 'history-force-set', sceneKey: '', enabled: 'clear' as any },
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
return {
|
|
260
|
+
matched: true,
|
|
261
|
+
valid: true,
|
|
262
|
+
command: { kind: 'history-force-set', sceneKey: args[1], enabled: 'clear' as any },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
217
265
|
if (args[0] === 'on' || args[0] === 'off') {
|
|
218
266
|
if (args.length === 1) {
|
|
219
267
|
return {
|
|
@@ -233,6 +281,50 @@ export function parseSceneAdminCommand(command: NativeCommand): ParsedSceneAdmin
|
|
|
233
281
|
valid: true,
|
|
234
282
|
command: { kind: 'history-force-get', sceneKey: args[0] },
|
|
235
283
|
};
|
|
284
|
+
case 'download-media':
|
|
285
|
+
if (args.length === 0) {
|
|
286
|
+
return { matched: true, valid: true, command: { kind: 'download-media-get' } };
|
|
287
|
+
}
|
|
288
|
+
if (args[0] === 'default') {
|
|
289
|
+
if (args[1] === 'on' || args[1] === 'off') {
|
|
290
|
+
return {
|
|
291
|
+
matched: true,
|
|
292
|
+
valid: true,
|
|
293
|
+
command: { kind: 'download-media-global-set', enabled: args[1] === 'on' },
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return { matched: true, valid: true, command: { kind: 'download-media-global-get' } };
|
|
297
|
+
}
|
|
298
|
+
if (args[0] === 'clear') {
|
|
299
|
+
return {
|
|
300
|
+
matched: true,
|
|
301
|
+
valid: true,
|
|
302
|
+
command: {
|
|
303
|
+
kind: 'download-media-set',
|
|
304
|
+
sceneKey: args[1] || '',
|
|
305
|
+
enabled: undefined as any,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
}
|
|
309
|
+
if (args[0] === 'on' || args[0] === 'off') {
|
|
310
|
+
if (args.length === 1) {
|
|
311
|
+
return {
|
|
312
|
+
matched: true,
|
|
313
|
+
valid: true,
|
|
314
|
+
command: { kind: 'download-media-set', sceneKey: '', enabled: args[0] === 'on' },
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
return {
|
|
318
|
+
matched: true,
|
|
319
|
+
valid: true,
|
|
320
|
+
command: { kind: 'download-media-set', sceneKey: args[1], enabled: args[0] === 'on' },
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
return {
|
|
324
|
+
matched: true,
|
|
325
|
+
valid: true,
|
|
326
|
+
command: { kind: 'download-media-get', sceneKey: args[0] },
|
|
327
|
+
};
|
|
236
328
|
case 'history-help':
|
|
237
329
|
return { matched: true, valid: true, command: { kind: 'history-help' } };
|
|
238
330
|
default:
|
|
@@ -399,6 +491,11 @@ export function executeSceneAdminCommand(args: {
|
|
|
399
491
|
}
|
|
400
492
|
const sExisting = sceneRegistry.get(sSceneKey);
|
|
401
493
|
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
494
|
+
if (command.limit === 'clear') {
|
|
495
|
+
const { historyLimit: _, ...rest } = sExisting;
|
|
496
|
+
sceneRegistry.set(sSceneKey, { ...rest, lastSeenAt: now() });
|
|
497
|
+
return { ok: true, text: `Cleared ${sSceneKey} history limit. Will use default (50).` };
|
|
498
|
+
}
|
|
402
499
|
const rawLimit = command.limit;
|
|
403
500
|
if (!Number.isFinite(rawLimit) || Number.isNaN(rawLimit)) {
|
|
404
501
|
return { ok: false, text: 'Invalid number.' };
|
|
@@ -443,6 +540,11 @@ export function executeSceneAdminCommand(args: {
|
|
|
443
540
|
}
|
|
444
541
|
const sExisting = sceneRegistry.get(sSceneKey);
|
|
445
542
|
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
543
|
+
if (command.enabled === 'clear') {
|
|
544
|
+
const { historyForce: _, ...rest } = sExisting;
|
|
545
|
+
sceneRegistry.set(sSceneKey, { ...rest, lastSeenAt: now() });
|
|
546
|
+
return { ok: true, text: `Cleared ${sSceneKey} history auto flush. Will use default (on).` };
|
|
547
|
+
}
|
|
446
548
|
sceneRegistry.set(sSceneKey, {
|
|
447
549
|
...sExisting,
|
|
448
550
|
historyForce: command.enabled,
|
|
@@ -454,6 +556,75 @@ export function executeSceneAdminCommand(args: {
|
|
|
454
556
|
};
|
|
455
557
|
}
|
|
456
558
|
|
|
559
|
+
if (command.kind === 'download-media-global-get') {
|
|
560
|
+
const g = sceneRegistry.get(GLOBAL_SCENE_KEY);
|
|
561
|
+
const enabled = g?.downloadMedia === true;
|
|
562
|
+
return { ok: true, text: `Global default download remote media is ${enabled ? 'on' : 'off'}.` };
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
if (command.kind === 'download-media-global-set') {
|
|
566
|
+
const g = sceneRegistry.get(GLOBAL_SCENE_KEY);
|
|
567
|
+
sceneRegistry.set(GLOBAL_SCENE_KEY, {
|
|
568
|
+
sceneKey: GLOBAL_SCENE_KEY,
|
|
569
|
+
kind: 'group',
|
|
570
|
+
status: 'allowed',
|
|
571
|
+
...(g || { platform: 'bncr' }),
|
|
572
|
+
downloadMedia: command.enabled,
|
|
573
|
+
lastSeenAt: now(),
|
|
574
|
+
});
|
|
575
|
+
return {
|
|
576
|
+
ok: true,
|
|
577
|
+
text: `Global default download remote media set to ${command.enabled ? 'on' : 'off'}.`,
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
if (command.kind === 'download-media-get') {
|
|
582
|
+
const qSceneKey = command.sceneKey || resolveCurrentGroupSceneKey(parsed);
|
|
583
|
+
if (!qSceneKey) {
|
|
584
|
+
return {
|
|
585
|
+
ok: true,
|
|
586
|
+
text: `Global default download remote media is ${sceneRegistry.get(GLOBAL_SCENE_KEY)?.downloadMedia === true ? 'on' : 'off'}.`,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
const s = sceneRegistry.get(qSceneKey);
|
|
590
|
+
if (!s) return { ok: true, text: 'Default download remote media is off.' };
|
|
591
|
+
const enabled = s.downloadMedia === true;
|
|
592
|
+
return {
|
|
593
|
+
ok: true,
|
|
594
|
+
text: `Current ${qSceneKey} download remote media is ${enabled ? 'on' : 'off'}.`,
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
if (command.kind === 'download-media-set') {
|
|
599
|
+
const sSceneKey = command.sceneKey || resolveCurrentGroupSceneKey(parsed);
|
|
600
|
+
if (!sSceneKey) {
|
|
601
|
+
return { ok: false, text: 'Current group shortcut only works inside a group chat.' };
|
|
602
|
+
}
|
|
603
|
+
const sExisting = sceneRegistry.get(sSceneKey);
|
|
604
|
+
if (command.enabled === undefined) {
|
|
605
|
+
// Clear per-group setting
|
|
606
|
+
if (sExisting) {
|
|
607
|
+
const { downloadMedia: _, ...rest } = sExisting;
|
|
608
|
+
sceneRegistry.set(sSceneKey, { ...rest, lastSeenAt: now() });
|
|
609
|
+
return {
|
|
610
|
+
ok: true,
|
|
611
|
+
text: `Cleared ${sSceneKey} download remote media. Will use global default.`,
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
return { ok: true, text: `No per-group config to clear for ${sSceneKey}.` };
|
|
615
|
+
}
|
|
616
|
+
if (!sExisting) return { ok: false, text: `Scene not found: ${sSceneKey}` };
|
|
617
|
+
sceneRegistry.set(sSceneKey, {
|
|
618
|
+
...sExisting,
|
|
619
|
+
downloadMedia: command.enabled,
|
|
620
|
+
lastSeenAt: now(),
|
|
621
|
+
});
|
|
622
|
+
return {
|
|
623
|
+
ok: true,
|
|
624
|
+
text: `Set ${sSceneKey} download remote media to ${command.enabled ? 'on' : 'off'}.`,
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
457
628
|
const sceneKey = command.sceneKey || resolveCurrentGroupSceneKey(parsed);
|
|
458
629
|
if (!sceneKey) {
|
|
459
630
|
return { ok: false, text: 'Current group shortcut only works inside a group chat.' };
|
|
@@ -482,9 +653,14 @@ export function executeSceneAdminCommand(args: {
|
|
|
482
653
|
if (existing.kind !== 'group') {
|
|
483
654
|
return { ok: false, text: `Scene ${sceneKey} is not a group scene.` };
|
|
484
655
|
}
|
|
656
|
+
if (command.mode === 'clear') {
|
|
657
|
+
const { groupReplyMode: _, ...rest } = existing;
|
|
658
|
+
sceneRegistry.set(sceneKey, { ...rest, lastSeenAt: now() });
|
|
659
|
+
return { ok: true, text: `Cleared ${sceneKey} reply mode. Will use default (admin).` };
|
|
660
|
+
}
|
|
485
661
|
sceneRegistry.set(sceneKey, {
|
|
486
662
|
...existing,
|
|
487
|
-
groupReplyMode: command.mode,
|
|
663
|
+
groupReplyMode: command.mode as BncrGroupReplyMode,
|
|
488
664
|
lastSeenAt: now(),
|
|
489
665
|
});
|
|
490
666
|
return { ok: true, text: `Set ${sceneKey} reply mode to ${command.mode}.` };
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* [BncrParam:JSON] marker parser.
|
|
3
|
+
*
|
|
4
|
+
* Extracts embedded JSON control parameters from agent text, removes the
|
|
5
|
+
* marker from the visible message, and returns the parsed key-value pairs.
|
|
6
|
+
*
|
|
7
|
+
* Format: [BncrParam:{"key":"val","asVoice":true}]
|
|
8
|
+
*
|
|
9
|
+
* Incomplete markers (missing closing ]) are left in text unchanged.
|
|
10
|
+
* Invalid JSON within a complete marker - marker is removed but no params.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const PREFIX = '[BncrParam:';
|
|
14
|
+
|
|
15
|
+
export function parseBncrMarker(text: string): {
|
|
16
|
+
cleanText: string;
|
|
17
|
+
params: Record<string, unknown>;
|
|
18
|
+
} {
|
|
19
|
+
const idx = text.indexOf(PREFIX);
|
|
20
|
+
if (idx === -1) return { cleanText: text, params: {} };
|
|
21
|
+
|
|
22
|
+
const start = idx + PREFIX.length;
|
|
23
|
+
if (start >= text.length) return { cleanText: text, params: {} };
|
|
24
|
+
|
|
25
|
+
// Scan character-by-character tracking brace/bracket depth so that
|
|
26
|
+
// ] inside JSON values (e.g. CDATA content) is not mistaken for
|
|
27
|
+
// the closing marker bracket.
|
|
28
|
+
let depth = 0;
|
|
29
|
+
let inString = false;
|
|
30
|
+
let esc = false;
|
|
31
|
+
let end = -1;
|
|
32
|
+
let started = false;
|
|
33
|
+
|
|
34
|
+
for (let i = start; i < text.length; i++) {
|
|
35
|
+
const ch = text[i];
|
|
36
|
+
|
|
37
|
+
if (esc) {
|
|
38
|
+
esc = false;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
if (ch === '\\' && inString) {
|
|
42
|
+
esc = true;
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
if (ch === '"') {
|
|
46
|
+
inString = !inString;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!inString) {
|
|
51
|
+
if (!started) {
|
|
52
|
+
if (ch === '{' || ch === '[') {
|
|
53
|
+
started = true;
|
|
54
|
+
depth = 1;
|
|
55
|
+
}
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (ch === '{' || ch === '[') {
|
|
60
|
+
depth++;
|
|
61
|
+
} else if (ch === '}' || ch === ']') {
|
|
62
|
+
depth--;
|
|
63
|
+
if (depth === 0) {
|
|
64
|
+
// JSON content complete - expect ] to close the marker
|
|
65
|
+
if (i + 1 < text.length && text[i + 1] === ']') {
|
|
66
|
+
end = i + 1;
|
|
67
|
+
}
|
|
68
|
+
break;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (end === -1) return { cleanText: text, params: {} };
|
|
75
|
+
|
|
76
|
+
const rawJson = text.slice(start, end);
|
|
77
|
+
let params: Record<string, unknown> = {};
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(rawJson);
|
|
81
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
82
|
+
params = parsed as Record<string, unknown>;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Invalid JSON — remove marker but don't apply params
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const cleanText = (text.slice(0, idx) + text.slice(end + 1)).replace(/ {2,}/g, ' ').trim();
|
|
89
|
+
return { cleanText, params };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Media types that the pipeline internally routes as media sends.
|
|
94
|
+
* Only these types are consumed (stripped) from extra.
|
|
95
|
+
* Non-media types like "appmsg" remain in extra for downstream passthrough.
|
|
96
|
+
*/
|
|
97
|
+
const MEDIA_CONSUMPTION_TYPES = new Set(['file', 'image', 'video', 'audio', 'voice']);
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Known fields that the bncr plugin unconditionally consumes during outbound
|
|
101
|
+
* processing. These are stripped from extra at entry and set as independent
|
|
102
|
+
* pipeline params. `type` is NOT in this set — it is consumed conditionally
|
|
103
|
+
* based on whether its value is a recognized media type.
|
|
104
|
+
*/
|
|
105
|
+
const CONSUMPTION_FIELDS_NO_TYPE = new Set([
|
|
106
|
+
'asVoice',
|
|
107
|
+
'audioAsVoice',
|
|
108
|
+
'kind',
|
|
109
|
+
'replyToId',
|
|
110
|
+
'downloadMedia',
|
|
111
|
+
]);
|
|
112
|
+
|
|
113
|
+
const CONSUMPTION_FIELDS = new Set([...CONSUMPTION_FIELDS_NO_TYPE]);
|
|
114
|
+
|
|
115
|
+
export function extractConsumptionFields(extra: Record<string, unknown> | undefined): {
|
|
116
|
+
consumed: Partial<{
|
|
117
|
+
asVoice: boolean;
|
|
118
|
+
audioAsVoice: boolean;
|
|
119
|
+
type: string;
|
|
120
|
+
kind: string;
|
|
121
|
+
replyToId: string;
|
|
122
|
+
downloadMedia: boolean;
|
|
123
|
+
}>;
|
|
124
|
+
remaining: Record<string, unknown>;
|
|
125
|
+
} {
|
|
126
|
+
const consumed: Record<string, unknown> = {};
|
|
127
|
+
const remaining: Record<string, unknown> = {};
|
|
128
|
+
|
|
129
|
+
if (!extra) return { consumed: {}, remaining: {} };
|
|
130
|
+
|
|
131
|
+
for (const [key, value] of Object.entries(extra)) {
|
|
132
|
+
// type is only consumed when its value is a recognized media type
|
|
133
|
+
// (file/image/video/audio/voice). Non-media types like "appmsg"
|
|
134
|
+
// stay in remaining for downstream passthrough.
|
|
135
|
+
if (key === 'type' && typeof value === 'string') {
|
|
136
|
+
if (MEDIA_CONSUMPTION_TYPES.has(value.trim().toLowerCase())) {
|
|
137
|
+
consumed[key] = value;
|
|
138
|
+
} else {
|
|
139
|
+
remaining[key] = value;
|
|
140
|
+
}
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (CONSUMPTION_FIELDS.has(key)) {
|
|
144
|
+
consumed[key] = value;
|
|
145
|
+
} else {
|
|
146
|
+
remaining[key] = value;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
consumed: consumed as Record<string, unknown>,
|
|
152
|
+
remaining,
|
|
153
|
+
};
|
|
154
|
+
}
|