@xmoxmo/bncr 0.4.1 → 0.4.3

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.4.1",
3
+ "version": "0.4.3",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -153,6 +153,10 @@ function ensurePluginNodeModulesLink(pluginDir: string, targetRoot: string) {
153
153
  }
154
154
 
155
155
  export function resolveBncrRuntimeSourceDir(pluginDir: string) {
156
+ const pluginRoot = resolveBncrPluginRoot(pluginDir);
157
+ const rootSource = path.join(pluginRoot, 'src');
158
+ if (fs.existsSync(path.join(rootSource, 'channel.ts'))) return rootSource;
159
+
156
160
  const direct = path.join(pluginDir, 'src');
157
161
  if (fs.existsSync(path.join(direct, 'channel.ts'))) return direct;
158
162
 
@@ -15,9 +15,30 @@ export type LoadedRuntime = {
15
15
  createBncrChannelPlugin: ChannelModule['createBncrChannelPlugin'];
16
16
  };
17
17
 
18
- export const pluginFile = fileURLToPath(new URL('../../index.ts', import.meta.url));
18
+ export function resolvePluginEntryFileFromModule(moduleUrl: string) {
19
+ const currentFile = fileURLToPath(moduleUrl);
20
+ const pluginRoot = resolveBncrPluginRoot(currentFile);
21
+ const currentDir = path.dirname(currentFile);
22
+ const distEntry = path.join(pluginRoot, 'dist', 'index.js');
23
+ if (currentFile === distEntry && fs.existsSync(distEntry)) return distEntry;
24
+
25
+ const sourceEntry = path.join(pluginRoot, 'index.ts');
26
+ if (fs.existsSync(sourceEntry)) return sourceEntry;
27
+
28
+ if (fs.existsSync(distEntry)) return distEntry;
29
+
30
+ if (path.basename(currentDir) === 'dist') return distEntry;
31
+
32
+ return sourceEntry;
33
+ }
34
+
35
+ function resolvePluginEntryFile() {
36
+ return resolvePluginEntryFileFromModule(import.meta.url);
37
+ }
38
+
39
+ export const pluginFile = resolvePluginEntryFile();
19
40
  export const pluginDir = path.dirname(pluginFile);
20
- export const pluginRequire = createRequire(new URL('../../index.ts', import.meta.url));
41
+ export const pluginRequire = createRequire(pluginFile);
21
42
  export const pluginRoot = resolveBncrPluginRoot(pluginFile);
22
43
 
23
44
  const runtimeSourceDir = resolveBncrRuntimeSourceDir(pluginDir);
package/src/channel.ts CHANGED
@@ -1164,6 +1164,7 @@ class BncrBridgeRuntime {
1164
1164
  asVoice?: boolean;
1165
1165
  audioAsVoice?: boolean;
1166
1166
  type?: string;
1167
+ extra?: Record<string, unknown>;
1167
1168
  kind?: 'tool' | 'block' | 'final';
1168
1169
  replyToId?: string;
1169
1170
  replyTargetPolicy?: OutboundReplyTargetPolicy;
@@ -1182,6 +1183,7 @@ class BncrBridgeRuntime {
1182
1183
  asVoice: params.asVoice,
1183
1184
  audioAsVoice: params.audioAsVoice,
1184
1185
  type: params.type,
1186
+ extra: params.extra,
1185
1187
  kind: params.kind,
1186
1188
  replyToId: asString(params.replyToId || '').trim() || undefined,
1187
1189
  replyTargetPolicy: params.replyTargetPolicy,
@@ -1245,6 +1247,7 @@ class BncrBridgeRuntime {
1245
1247
  mimeType: params.media.mimeType,
1246
1248
  }),
1247
1249
  hintedType: wantsVoice ? 'voice' : asString(params.meta.type || '') || undefined,
1250
+ extra: params.meta.extra as Record<string, unknown> | undefined,
1248
1251
  kind: messageKind,
1249
1252
  replyToId: normalizeReplyToId(params.meta.replyToId) || undefined,
1250
1253
  now: now(),
@@ -16,6 +16,7 @@ export function buildFileTransferOutboxEntry(args: {
16
16
  asVoice?: boolean;
17
17
  audioAsVoice?: boolean;
18
18
  type?: string;
19
+ extra?: Record<string, unknown>;
19
20
  kind?: 'tool' | 'block' | 'final';
20
21
  replyToId?: string;
21
22
  replyTargetPolicy?: OutboundReplyTargetPolicy;
@@ -38,6 +39,7 @@ export function buildFileTransferOutboxEntry(args: {
38
39
  asVoice: args.asVoice === true,
39
40
  audioAsVoice: args.audioAsVoice === true,
40
41
  type: args.type,
42
+ ...(args.extra ? { extra: { ...args.extra } } : {}),
41
43
  finalEvent: args.pushEvent,
42
44
  replyToId:
43
45
  normalizeOutboundReplyToId({
@@ -12,6 +12,7 @@ type MinimalBncrSendInput = {
12
12
  media?: string;
13
13
  filePath?: string;
14
14
  mediaUrl?: string;
15
+ mediaUrls?: string[];
15
16
  asVoice?: boolean;
16
17
  audioAsVoice?: boolean;
17
18
  params?: Record<string, unknown>;
@@ -73,13 +74,19 @@ export function buildBncrMessageAction(input: MinimalBncrSendInput): BuiltBncrMe
73
74
  input.filePath,
74
75
  input.mediaUrl,
75
76
  );
77
+ const rawMediaUrls = Array.isArray(paramsObj.mediaUrls)
78
+ ? paramsObj.mediaUrls
79
+ : Array.isArray(input.mediaUrls)
80
+ ? input.mediaUrls
81
+ : undefined;
82
+ const mediaUrls = rawMediaUrls?.map((value) => asString(value || '').trim()).filter(Boolean);
76
83
 
77
84
  const message = pickFirstString(paramsObj.message, input.message) ?? '';
78
85
  const explicitCaption = pickFirstString(paramsObj.caption, input.caption) ?? '';
79
86
  const asVoice = pickFirstBoolean(paramsObj.asVoice, input.asVoice);
80
87
  const audioAsVoice = pickFirstBoolean(paramsObj.audioAsVoice, input.audioAsVoice);
81
88
 
82
- if ((asVoice === true || audioAsVoice === true) && !mediaPath) {
89
+ if ((asVoice === true || audioAsVoice === true) && !mediaPath && !mediaUrls?.length) {
83
90
  throw new Error('bncr voice send requires media path');
84
91
  }
85
92
 
@@ -93,11 +100,20 @@ export function buildBncrMessageAction(input: MinimalBncrSendInput): BuiltBncrMe
93
100
  const finalCaption = explicitCaption || message;
94
101
  if (finalCaption) normalizedParams.caption = finalCaption;
95
102
  delete normalizedParams.message;
103
+ if (mediaUrls?.length) normalizedParams.mediaUrls = mediaUrls;
96
104
  } else {
97
- const finalMessage = message || explicitCaption;
98
- if (!finalMessage.trim()) throw new Error('bncr send requires message or media');
99
- normalizedParams.message = finalMessage;
100
- delete normalizedParams.caption;
105
+ if (mediaUrls?.length) {
106
+ normalizedParams.mediaUrls = mediaUrls;
107
+ const finalCaption = explicitCaption || message;
108
+ if (finalCaption) normalizedParams.caption = finalCaption;
109
+ delete normalizedParams.message;
110
+ delete normalizedParams.path;
111
+ } else {
112
+ const finalMessage = message || explicitCaption;
113
+ if (!finalMessage.trim()) throw new Error('bncr send requires message or media');
114
+ normalizedParams.message = finalMessage;
115
+ delete normalizedParams.caption;
116
+ }
101
117
  }
102
118
 
103
119
  if (asVoice === true) normalizedParams.asVoice = true;
@@ -68,6 +68,7 @@ export function buildBncrMediaOutboundFrame(params: {
68
68
  mediaMsg: string;
69
69
  fileName: string;
70
70
  hintedType?: string;
71
+ extra?: Record<string, unknown>;
71
72
  kind?: 'tool' | 'block' | 'final';
72
73
  replyToId?: string;
73
74
  now: number;
@@ -96,6 +97,7 @@ export function buildBncrMediaOutboundFrame(params: {
96
97
  base64: params.media.mediaBase64 || '',
97
98
  fileName: params.fileName,
98
99
  transferMode: params.media.mode,
100
+ ...(params.extra ? { extra: { ...params.extra } } : {}),
99
101
  },
100
102
  ts: params.now,
101
103
  };
@@ -62,6 +62,7 @@ export function enqueueReplyMediaFileTransferEntry(
62
62
  asVoice: boolean;
63
63
  audioAsVoice: boolean;
64
64
  type?: string;
65
+ extra?: Record<string, unknown>;
65
66
  kind?: 'tool' | 'block' | 'final';
66
67
  replyToId?: string;
67
68
  replyTargetPolicy?: OutboundReplyTargetPolicy;
@@ -86,6 +87,7 @@ export function enqueueReplyMediaFileTransferEntry(
86
87
  asVoice: params.asVoice,
87
88
  audioAsVoice: params.audioAsVoice,
88
89
  type: params.type,
90
+ extra: params.extra,
89
91
  kind: params.kind,
90
92
  replyToId: params.replyToId || undefined,
91
93
  replyTargetPolicy: params.replyTargetPolicy,
@@ -132,6 +134,7 @@ export function enqueueSingleReplyMediaEntry(
132
134
  asVoice: params.params.payload.asVoice,
133
135
  audioAsVoice: params.params.payload.audioAsVoice,
134
136
  type: params.params.payload.type,
137
+ extra: params.params.payload.extra,
135
138
  kind: params.params.payload.kind,
136
139
  replyToId: params.params.payload.replyToId,
137
140
  replyTargetPolicy: params.params.payload.replyTargetPolicy,
@@ -19,6 +19,7 @@ export type ReplyPayloadInput = {
19
19
  asVoice?: boolean;
20
20
  audioAsVoice?: boolean;
21
21
  type?: string;
22
+ extra?: Record<string, unknown>;
22
23
  kind?: 'tool' | 'block' | 'final';
23
24
  replyToId?: string;
24
25
  };
@@ -31,6 +32,7 @@ export type NormalizedReplyPayload = {
31
32
  asVoice: boolean;
32
33
  audioAsVoice: boolean;
33
34
  type?: string;
35
+ extra?: Record<string, unknown>;
34
36
  kind?: 'tool' | 'block' | 'final';
35
37
  replyToId: string;
36
38
  replyTargetPolicy: OutboundReplyTargetPolicy;
@@ -64,6 +66,7 @@ export type ReplyMediaFileTransferParams = {
64
66
  asVoice: boolean;
65
67
  audioAsVoice: boolean;
66
68
  type?: string;
69
+ extra?: Record<string, unknown>;
67
70
  kind?: 'tool' | 'block' | 'final';
68
71
  replyToId: string;
69
72
  replyTargetPolicy: OutboundReplyTargetPolicy;
@@ -268,6 +271,7 @@ export function normalizeReplyPayload(
268
271
  asVoice: payload?.asVoice === true,
269
272
  audioAsVoice: payload?.audioAsVoice === true,
270
273
  ...(type ? { type } : {}),
274
+ ...(payload?.extra ? { extra: { ...payload.extra } } : {}),
271
275
  kind: payload?.kind,
272
276
  replyTargetPolicy: options?.replyTargetPolicy ?? 'agent-default',
273
277
  replyToId: normalizeOutboundReplyToId({
@@ -7,9 +7,11 @@ export type NormalizedBncrSendParams = {
7
7
  message: string;
8
8
  caption: string;
9
9
  mediaUrl?: string;
10
+ mediaUrls?: string[];
10
11
  asVoice: boolean;
11
12
  audioAsVoice: boolean;
12
13
  type?: string;
14
+ extra?: Record<string, unknown>;
13
15
  };
14
16
 
15
17
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -33,16 +35,34 @@ export function normalizeBncrSendParams(input: {
33
35
  readOpenClawStringParam(paramsObj, 'path', { trim: false }) ??
34
36
  readOpenClawStringParam(paramsObj, 'filePath', { trim: false }) ??
35
37
  readOpenClawStringParam(paramsObj, 'mediaUrl', { trim: false });
38
+ const rawMediaUrls = paramsObj.mediaUrls;
39
+ const mediaUrls = Array.isArray(rawMediaUrls)
40
+ ? Array.from(
41
+ new Set(rawMediaUrls.map((v) => (typeof v === 'string' ? v.trim() : '')).filter(Boolean)),
42
+ )
43
+ : undefined;
44
+ // 如果 mediaUrl 已经在 mediaUrls 中,去重避免重复发送
45
+ const dedupedMediaUrls = mediaUrls?.length
46
+ ? mediaUrl && mediaUrls.includes(mediaUrl)
47
+ ? mediaUrls
48
+ : mediaUrl
49
+ ? [mediaUrl, ...mediaUrls]
50
+ : mediaUrls
51
+ : undefined;
36
52
  const asVoice = readOpenClawBooleanParam(paramsObj, 'asVoice') ?? false;
37
53
  const audioAsVoice = readOpenClawBooleanParam(paramsObj, 'audioAsVoice') ?? false;
38
54
  const type = readOpenClawStringParam(paramsObj, 'type') || undefined;
55
+ const rawExtra = paramsObj.extra;
56
+ const extra = isPlainObject(rawExtra) ? { ...rawExtra } : undefined;
39
57
 
40
- if (asVoice && !mediaUrl) throw new Error('send voice requires media path');
58
+ const hasMedia = Boolean(mediaUrl || dedupedMediaUrls?.length);
41
59
 
42
- const normalizedMessage = mediaUrl ? '' : message || caption || '';
43
- const normalizedCaption = mediaUrl ? caption || message || '' : '';
60
+ if (asVoice && !hasMedia) throw new Error('send voice requires media path');
44
61
 
45
- if (!normalizedMessage.trim() && !normalizedCaption.trim() && !mediaUrl) {
62
+ const normalizedMessage = hasMedia ? '' : message || caption || '';
63
+ const normalizedCaption = hasMedia ? caption || message || '' : '';
64
+
65
+ if (!normalizedMessage.trim() && !normalizedCaption.trim() && !hasMedia) {
46
66
  throw new Error('send requires message or media');
47
67
  }
48
68
 
@@ -51,9 +71,11 @@ export function normalizeBncrSendParams(input: {
51
71
  accountId: resolvedAccountId,
52
72
  message: normalizedMessage,
53
73
  caption: normalizedCaption,
54
- mediaUrl: mediaUrl || undefined,
74
+ mediaUrl: dedupedMediaUrls?.length ? undefined : mediaUrl || undefined,
75
+ mediaUrls: dedupedMediaUrls,
55
76
  asVoice,
56
77
  audioAsVoice,
57
78
  ...(type ? { type } : {}),
79
+ ...(extra ? { extra } : {}),
58
80
  };
59
81
  }
@@ -64,6 +64,7 @@ export async function sendBncrMedia(params: {
64
64
  asVoice?: boolean;
65
65
  audioAsVoice?: boolean;
66
66
  type?: string;
67
+ extra?: Record<string, unknown>;
67
68
  kind?: string;
68
69
  replyToId?: string;
69
70
  mediaLocalRoots?: readonly string[];
@@ -83,6 +84,7 @@ export async function sendBncrMedia(params: {
83
84
  asVoice?: boolean;
84
85
  audioAsVoice?: boolean;
85
86
  type?: string;
87
+ extra?: Record<string, unknown>;
86
88
  kind?: 'tool' | 'block' | 'final';
87
89
  replyToId?: string;
88
90
  };
@@ -104,6 +106,7 @@ export async function sendBncrMedia(params: {
104
106
  asVoice: params.asVoice === true ? true : undefined,
105
107
  audioAsVoice: params.audioAsVoice === true ? true : undefined,
106
108
  type: params.type,
109
+ extra: params.extra,
107
110
  kind: normalizeReplyKind(params.kind),
108
111
  replyToId: params.replyToId,
109
112
  },
@@ -140,37 +140,40 @@ export function createBncrChannelPluginSurfaceGroup(runtime: {
140
140
  const normalized = normalizeBncrSendParams({ params, accountId: accountId || '' });
141
141
 
142
142
  const toolActionBridge = runtime.getToolActionBridge();
143
- const result = normalized.mediaUrl
144
- ? await sendBncrMedia({
145
- channelId: runtime.channelId,
146
- accountId: normalized.accountId,
147
- to: normalized.to,
148
- text: normalized.caption,
149
- mediaUrl: normalized.mediaUrl,
150
- asVoice: normalized.asVoice,
151
- audioAsVoice: normalized.audioAsVoice,
152
- type: normalized.type,
153
- mediaLocalRoots,
154
- resolveVerifiedTarget: (to, accountId) =>
155
- toolActionBridge.resolveVerifiedTarget(to, accountId),
156
- rememberSessionRoute: (sessionKey, accountId, route) =>
157
- toolActionBridge.rememberSessionRoute(sessionKey, accountId, route),
158
- enqueueFromReply: (args) => toolActionBridge.enqueueFromReply(args),
159
- createMessageId: () => randomUUID(),
160
- })
161
- : await sendBncrText({
162
- channelId: runtime.channelId,
163
- accountId: normalized.accountId,
164
- to: normalized.to,
165
- text: normalized.message,
166
- mediaLocalRoots,
167
- resolveVerifiedTarget: (to, accountId) =>
168
- toolActionBridge.resolveVerifiedTarget(to, accountId),
169
- rememberSessionRoute: (sessionKey, accountId, route) =>
170
- toolActionBridge.rememberSessionRoute(sessionKey, accountId, route),
171
- enqueueFromReply: (args) => toolActionBridge.enqueueFromReply(args),
172
- createMessageId: () => randomUUID(),
173
- });
143
+ const result =
144
+ normalized.mediaUrl || normalized.mediaUrls?.length
145
+ ? await sendBncrMedia({
146
+ channelId: runtime.channelId,
147
+ accountId: normalized.accountId,
148
+ to: normalized.to,
149
+ text: normalized.caption,
150
+ mediaUrl: normalized.mediaUrl,
151
+ mediaUrls: normalized.mediaUrls,
152
+ asVoice: normalized.asVoice,
153
+ audioAsVoice: normalized.audioAsVoice,
154
+ type: normalized.type,
155
+ extra: normalized.extra,
156
+ mediaLocalRoots,
157
+ resolveVerifiedTarget: (to, accountId) =>
158
+ toolActionBridge.resolveVerifiedTarget(to, accountId),
159
+ rememberSessionRoute: (sessionKey, accountId, route) =>
160
+ toolActionBridge.rememberSessionRoute(sessionKey, accountId, route),
161
+ enqueueFromReply: (args) => toolActionBridge.enqueueFromReply(args),
162
+ createMessageId: () => randomUUID(),
163
+ })
164
+ : await sendBncrText({
165
+ channelId: runtime.channelId,
166
+ accountId: normalized.accountId,
167
+ to: normalized.to,
168
+ text: normalized.message,
169
+ mediaLocalRoots,
170
+ resolveVerifiedTarget: (to, accountId) =>
171
+ toolActionBridge.resolveVerifiedTarget(to, accountId),
172
+ rememberSessionRoute: (sessionKey, accountId, route) =>
173
+ toolActionBridge.rememberSessionRoute(sessionKey, accountId, route),
174
+ enqueueFromReply: (args) => toolActionBridge.enqueueFromReply(args),
175
+ createMessageId: () => randomUUID(),
176
+ });
174
177
 
175
178
  return runtime.openClawJsonResult({ ok: true, ...result });
176
179
  },
@@ -337,6 +337,7 @@ export function buildBncrMediaOrchestratorsRuntime(deps: {
337
337
  asVoice: boolean;
338
338
  audioAsVoice: boolean;
339
339
  type?: string;
340
+ extra?: Record<string, unknown>;
340
341
  kind?: 'tool' | 'block' | 'final';
341
342
  replyToId?: string;
342
343
  replyTargetPolicy?: OutboundReplyTargetPolicy;
@@ -49,6 +49,7 @@ function buildReplyMediaEntryHelpers(runtime: {
49
49
  asVoice: boolean;
50
50
  audioAsVoice: boolean;
51
51
  type?: string;
52
+ extra?: Record<string, unknown>;
52
53
  kind?: 'tool' | 'block' | 'final';
53
54
  replyToId?: string;
54
55
  replyTargetPolicy?: OutboundReplyTargetPolicy;
@@ -203,6 +204,7 @@ export function createBncrMediaOrchestratorsRuntimeGroup(runtime: {
203
204
  asVoice: boolean;
204
205
  audioAsVoice: boolean;
205
206
  type?: string;
207
+ extra?: Record<string, unknown>;
206
208
  kind?: 'tool' | 'block' | 'final';
207
209
  replyToId?: string;
208
210
  replyTargetPolicy?: OutboundReplyTargetPolicy;