@xmoxmo/bncr 0.0.6 → 0.0.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/README.md
CHANGED
|
@@ -37,7 +37,20 @@ openclaw gateway restart
|
|
|
37
37
|
|
|
38
38
|
---
|
|
39
39
|
|
|
40
|
-
## 3.
|
|
40
|
+
## 3. 客户端接入流程(最简)
|
|
41
|
+
|
|
42
|
+
1. 在客户端插件配置中,将 **OpenClaw Token** 填写为 **gateway token**,并正确填写 host / port / ssl 后启用插件。
|
|
43
|
+
2. 启动(或重启)bncr 客户端后,在 OpenClaw 侧执行:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
openclaw devices approve --latest
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
完成后,客户端会使用自己的身份并自动保存后续授权。
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 4. 当前能力
|
|
41
54
|
|
|
42
55
|
- 文本
|
|
43
56
|
- 图片
|
|
@@ -54,7 +67,7 @@ openclaw gateway restart
|
|
|
54
67
|
|
|
55
68
|
---
|
|
56
69
|
|
|
57
|
-
##
|
|
70
|
+
## 5. 架构定位
|
|
58
71
|
|
|
59
72
|
bncr 当前采用两层模型:
|
|
60
73
|
|
|
@@ -77,7 +90,7 @@ plugins/bncr/src/
|
|
|
77
90
|
|
|
78
91
|
---
|
|
79
92
|
|
|
80
|
-
##
|
|
93
|
+
## 6. 配置项总览
|
|
81
94
|
|
|
82
95
|
当前主要配置字段:
|
|
83
96
|
|
|
@@ -98,7 +111,7 @@ plugins/bncr/src/
|
|
|
98
111
|
|
|
99
112
|
---
|
|
100
113
|
|
|
101
|
-
##
|
|
114
|
+
## 7. 状态与诊断
|
|
102
115
|
|
|
103
116
|
常用检查:
|
|
104
117
|
|
|
@@ -116,7 +129,7 @@ openclaw health --json
|
|
|
116
129
|
|
|
117
130
|
---
|
|
118
131
|
|
|
119
|
-
##
|
|
132
|
+
## 8. 自检与测试
|
|
120
133
|
|
|
121
134
|
```bash
|
|
122
135
|
cd plugins/bncr
|
|
@@ -133,11 +146,12 @@ npm pack
|
|
|
133
146
|
|
|
134
147
|
---
|
|
135
148
|
|
|
136
|
-
##
|
|
149
|
+
## 9. 上线前检查
|
|
137
150
|
|
|
138
151
|
上线前建议至少确认:
|
|
139
152
|
|
|
140
153
|
- README 与当前实现一致
|
|
154
|
+
- **隐私清理**:测试/示例/日志中的 scope、ID、账号等做去标识化(必要时用占位值)
|
|
141
155
|
- 配置 schema 与实际字段一致
|
|
142
156
|
- 测试通过
|
|
143
157
|
- 自检通过
|
package/package.json
CHANGED
package/src/channel.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
OpenClawPluginServiceContext,
|
|
7
7
|
GatewayRequestHandlerOptions,
|
|
8
8
|
ChatType,
|
|
9
|
+
ChannelMessageActionAdapter,
|
|
9
10
|
} from 'openclaw/plugin-sdk';
|
|
10
11
|
import {
|
|
11
12
|
createDefaultChannelRuntimeState,
|
|
@@ -13,6 +14,10 @@ import {
|
|
|
13
14
|
applyAccountNameToChannelSection,
|
|
14
15
|
writeJsonFileAtomically,
|
|
15
16
|
readJsonFileWithFallback,
|
|
17
|
+
readStringParam,
|
|
18
|
+
readBooleanParam,
|
|
19
|
+
extractToolSend,
|
|
20
|
+
jsonResult,
|
|
16
21
|
} from 'openclaw/plugin-sdk';
|
|
17
22
|
import { CHANNEL_ID, BNCR_DEFAULT_ACCOUNT_ID, normalizeAccountId, resolveDefaultDisplayName, resolveAccount, listAccountIds } from './core/accounts.js';
|
|
18
23
|
import type { BncrRoute, BncrConnection, OutboxEntry } from './core/types.js';
|
|
@@ -290,11 +295,17 @@ class BncrBridgeRuntime {
|
|
|
290
295
|
return map.get(normalizeAccountId(accountId)) || 0;
|
|
291
296
|
}
|
|
292
297
|
|
|
293
|
-
private syncDebugFlag() {
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
+
private async syncDebugFlag() {
|
|
299
|
+
try {
|
|
300
|
+
const cfg = await this.api.runtime.config.loadConfig();
|
|
301
|
+
const raw = (cfg as any)?.channels?.[CHANNEL_ID]?.debug?.verbose;
|
|
302
|
+
if (typeof raw !== 'boolean') return;
|
|
303
|
+
if (raw !== BNCR_DEBUG_VERBOSE) {
|
|
304
|
+
BNCR_DEBUG_VERBOSE = raw;
|
|
305
|
+
this.api.logger.info?.(`[bncr-debug] verbose=${BNCR_DEBUG_VERBOSE}`);
|
|
306
|
+
}
|
|
307
|
+
} catch {
|
|
308
|
+
// ignore config read errors
|
|
298
309
|
}
|
|
299
310
|
}
|
|
300
311
|
|
|
@@ -1593,11 +1604,11 @@ class BncrBridgeRuntime {
|
|
|
1593
1604
|
};
|
|
1594
1605
|
}
|
|
1595
1606
|
|
|
1596
|
-
|
|
1607
|
+
public async enqueueFromReply(params: {
|
|
1597
1608
|
accountId: string;
|
|
1598
1609
|
sessionKey: string;
|
|
1599
1610
|
route: BncrRoute;
|
|
1600
|
-
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
1611
|
+
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; asVoice?: boolean; audioAsVoice?: boolean };
|
|
1601
1612
|
mediaLocalRoots?: readonly string[];
|
|
1602
1613
|
}) {
|
|
1603
1614
|
const { accountId, sessionKey, route, payload, mediaLocalRoots } = params;
|
|
@@ -1620,6 +1631,7 @@ class BncrBridgeRuntime {
|
|
|
1620
1631
|
});
|
|
1621
1632
|
const messageId = randomUUID();
|
|
1622
1633
|
const mediaMsg = first ? asString(payload.text || '') : '';
|
|
1634
|
+
const wantsVoice = payload.asVoice === true || payload.audioAsVoice === true;
|
|
1623
1635
|
const frame = buildBncrMediaOutboundFrame({
|
|
1624
1636
|
messageId,
|
|
1625
1637
|
sessionKey,
|
|
@@ -1632,6 +1644,7 @@ class BncrBridgeRuntime {
|
|
|
1632
1644
|
fileName: media.fileName,
|
|
1633
1645
|
mimeType: media.mimeType,
|
|
1634
1646
|
}),
|
|
1647
|
+
hintedType: wantsVoice ? 'voice' : undefined,
|
|
1635
1648
|
now: now(),
|
|
1636
1649
|
});
|
|
1637
1650
|
|
|
@@ -1785,7 +1798,7 @@ class BncrBridgeRuntime {
|
|
|
1785
1798
|
};
|
|
1786
1799
|
|
|
1787
1800
|
handleActivity = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
1788
|
-
this.syncDebugFlag();
|
|
1801
|
+
void this.syncDebugFlag();
|
|
1789
1802
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
1790
1803
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
1791
1804
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
@@ -2287,6 +2300,8 @@ class BncrBridgeRuntime {
|
|
|
2287
2300
|
channelSendMedia = async (ctx: any) => {
|
|
2288
2301
|
const accountId = normalizeAccountId(ctx.accountId);
|
|
2289
2302
|
const to = asString(ctx.to || '').trim();
|
|
2303
|
+
const asVoice = ctx?.asVoice === true;
|
|
2304
|
+
const audioAsVoice = ctx?.audioAsVoice === true;
|
|
2290
2305
|
|
|
2291
2306
|
if (BNCR_DEBUG_VERBOSE) {
|
|
2292
2307
|
this.api.logger.info?.(
|
|
@@ -2296,6 +2311,8 @@ class BncrBridgeRuntime {
|
|
|
2296
2311
|
text: asString(ctx?.text || ''),
|
|
2297
2312
|
mediaUrl: asString(ctx?.mediaUrl || ''),
|
|
2298
2313
|
mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
|
|
2314
|
+
asVoice,
|
|
2315
|
+
audioAsVoice,
|
|
2299
2316
|
sessionKey: asString(ctx?.sessionKey || ''),
|
|
2300
2317
|
mirrorSessionKey: asString(ctx?.mirror?.sessionKey || ''),
|
|
2301
2318
|
rawCtx: {
|
|
@@ -2314,6 +2331,8 @@ class BncrBridgeRuntime {
|
|
|
2314
2331
|
to,
|
|
2315
2332
|
text: asString(ctx.text || ''),
|
|
2316
2333
|
mediaUrl: asString(ctx.mediaUrl || ''),
|
|
2334
|
+
asVoice,
|
|
2335
|
+
audioAsVoice,
|
|
2317
2336
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2318
2337
|
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2319
2338
|
rememberSessionRoute: (sessionKey, accountId, route) => this.rememberSessionRoute(sessionKey, accountId, route),
|
|
@@ -2328,6 +2347,58 @@ export function createBncrBridge(api: OpenClawPluginApi) {
|
|
|
2328
2347
|
}
|
|
2329
2348
|
|
|
2330
2349
|
export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
2350
|
+
const messageActions: ChannelMessageActionAdapter = {
|
|
2351
|
+
listActions: () => ['send'],
|
|
2352
|
+
supportsAction: ({ action }) => action === 'send',
|
|
2353
|
+
extractToolSend: ({ args }) => extractToolSend(args, 'sendMessage'),
|
|
2354
|
+
handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
|
|
2355
|
+
if (action !== 'send') throw new Error(`Action ${action} is not supported for provider ${CHANNEL_ID}.`);
|
|
2356
|
+
const to = readStringParam(params, 'to', { required: true });
|
|
2357
|
+
const message = readStringParam(params, 'message', { allowEmpty: true }) ?? '';
|
|
2358
|
+
const caption = readStringParam(params, 'caption', { allowEmpty: true }) ?? '';
|
|
2359
|
+
const content = message || caption || '';
|
|
2360
|
+
const mediaUrl =
|
|
2361
|
+
readStringParam(params, 'media', { trim: false }) ??
|
|
2362
|
+
readStringParam(params, 'path', { trim: false }) ??
|
|
2363
|
+
readStringParam(params, 'filePath', { trim: false }) ??
|
|
2364
|
+
readStringParam(params, 'mediaUrl', { trim: false });
|
|
2365
|
+
const asVoice = readBooleanParam(params, 'asVoice') ?? false;
|
|
2366
|
+
const audioAsVoice = readBooleanParam(params, 'audioAsVoice') ?? false;
|
|
2367
|
+
const resolvedAccountId = normalizeAccountId(readStringParam(params, 'accountId') ?? accountId);
|
|
2368
|
+
|
|
2369
|
+
if (!content.trim() && !mediaUrl) throw new Error('send requires text or media');
|
|
2370
|
+
|
|
2371
|
+
const result = mediaUrl
|
|
2372
|
+
? await sendBncrMedia({
|
|
2373
|
+
channelId: CHANNEL_ID,
|
|
2374
|
+
accountId: resolvedAccountId,
|
|
2375
|
+
to,
|
|
2376
|
+
text: content,
|
|
2377
|
+
mediaUrl,
|
|
2378
|
+
asVoice,
|
|
2379
|
+
audioAsVoice,
|
|
2380
|
+
mediaLocalRoots,
|
|
2381
|
+
resolveVerifiedTarget: (to, accountId) => bridge.resolveVerifiedTarget(to, accountId),
|
|
2382
|
+
rememberSessionRoute: (sessionKey, accountId, route) => bridge.rememberSessionRoute(sessionKey, accountId, route),
|
|
2383
|
+
enqueueFromReply: (args) => bridge.enqueueFromReply(args as any),
|
|
2384
|
+
createMessageId: () => randomUUID(),
|
|
2385
|
+
})
|
|
2386
|
+
: await sendBncrText({
|
|
2387
|
+
channelId: CHANNEL_ID,
|
|
2388
|
+
accountId: resolvedAccountId,
|
|
2389
|
+
to,
|
|
2390
|
+
text: content,
|
|
2391
|
+
mediaLocalRoots,
|
|
2392
|
+
resolveVerifiedTarget: (to, accountId) => bridge.resolveVerifiedTarget(to, accountId),
|
|
2393
|
+
rememberSessionRoute: (sessionKey, accountId, route) => bridge.rememberSessionRoute(sessionKey, accountId, route),
|
|
2394
|
+
enqueueFromReply: (args) => bridge.enqueueFromReply(args as any),
|
|
2395
|
+
createMessageId: () => randomUUID(),
|
|
2396
|
+
});
|
|
2397
|
+
|
|
2398
|
+
return jsonResult({ ok: true, ...result });
|
|
2399
|
+
},
|
|
2400
|
+
};
|
|
2401
|
+
|
|
2331
2402
|
const plugin = {
|
|
2332
2403
|
id: CHANNEL_ID,
|
|
2333
2404
|
meta: {
|
|
@@ -2338,6 +2409,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2338
2409
|
blurb: 'Bncr Channel.',
|
|
2339
2410
|
aliases: ['bncr'],
|
|
2340
2411
|
},
|
|
2412
|
+
actions: messageActions,
|
|
2341
2413
|
capabilities: {
|
|
2342
2414
|
chatTypes: ['direct'] as ChatType[],
|
|
2343
2415
|
media: true,
|
|
@@ -113,7 +113,7 @@ export async function handleBncrNativeCommand(params: {
|
|
|
113
113
|
disableBlockStreaming: true,
|
|
114
114
|
},
|
|
115
115
|
dispatcherOptions: {
|
|
116
|
-
deliver: async (payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] }, info?: { kind?: 'tool' | 'block' | 'final' }) => {
|
|
116
|
+
deliver: async (payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; audioAsVoice?: boolean }, info?: { kind?: 'tool' | 'block' | 'final' }) => {
|
|
117
117
|
if (info?.kind && info.kind !== 'final') return;
|
|
118
118
|
const hasPayload = Boolean(payload?.text || payload?.mediaUrl || (Array.isArray(payload?.mediaUrls) && payload.mediaUrls.length > 0));
|
|
119
119
|
if (!hasPayload) return;
|
|
@@ -151,7 +151,7 @@ export async function dispatchBncrInbound(params: {
|
|
|
151
151
|
},
|
|
152
152
|
dispatcherOptions: {
|
|
153
153
|
deliver: async (
|
|
154
|
-
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] },
|
|
154
|
+
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; audioAsVoice?: boolean },
|
|
155
155
|
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
156
156
|
) => {
|
|
157
157
|
if (info?.kind && info.kind !== 'final') return;
|
|
@@ -4,6 +4,11 @@ function asString(v: unknown, fallback = ''): string {
|
|
|
4
4
|
return String(v);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
function isAudioMimeType(mimeType?: string): boolean {
|
|
8
|
+
const mt = asString(mimeType || '').toLowerCase();
|
|
9
|
+
return mt.startsWith('audio/');
|
|
10
|
+
}
|
|
11
|
+
|
|
7
12
|
export function resolveBncrOutboundMessageType(params: { mimeType?: string; fileName?: string; hintedType?: string; hasPayload?: boolean }): 'text' | 'image' | 'video' | 'voice' | 'audio' | 'file' {
|
|
8
13
|
const hinted = asString(params.hintedType || '').toLowerCase();
|
|
9
14
|
const hasPayload = !!params.hasPayload;
|
|
@@ -12,6 +17,11 @@ export function resolveBncrOutboundMessageType(params: { mimeType?: string; file
|
|
|
12
17
|
const isStandard = hinted === 'text' || hinted === 'image' || hinted === 'video' || hinted === 'voice' || hinted === 'audio' || hinted === 'file';
|
|
13
18
|
|
|
14
19
|
if (hasPayload && major === 'text' && (hinted === 'text' || !isStandard)) return 'file';
|
|
20
|
+
if (hinted === 'voice') {
|
|
21
|
+
if (isAudioMimeType(mt)) return 'voice';
|
|
22
|
+
if (major === 'text' || major === 'image' || major === 'video' || major === 'audio') return major as any;
|
|
23
|
+
return 'file';
|
|
24
|
+
}
|
|
15
25
|
if (isStandard) return hinted as any;
|
|
16
26
|
if (major === 'text' || major === 'image' || major === 'video' || major === 'audio') return major as any;
|
|
17
27
|
return 'file';
|
|
@@ -25,6 +35,7 @@ export function buildBncrMediaOutboundFrame(params: {
|
|
|
25
35
|
mediaUrl: string;
|
|
26
36
|
mediaMsg: string;
|
|
27
37
|
fileName: string;
|
|
38
|
+
hintedType?: string;
|
|
28
39
|
now: number;
|
|
29
40
|
}) {
|
|
30
41
|
return {
|
|
@@ -40,6 +51,7 @@ export function buildBncrMediaOutboundFrame(params: {
|
|
|
40
51
|
mimeType: params.media.mimeType,
|
|
41
52
|
fileName: params.media.fileName,
|
|
42
53
|
hasPayload: !!(params.media.path || params.media.mediaBase64),
|
|
54
|
+
hintedType: params.hintedType,
|
|
43
55
|
}),
|
|
44
56
|
mimeType: params.media.mimeType || '',
|
|
45
57
|
msg: params.mediaMsg,
|
|
@@ -37,6 +37,8 @@ export async function sendBncrMedia(params: {
|
|
|
37
37
|
to: string;
|
|
38
38
|
text?: string;
|
|
39
39
|
mediaUrl?: string;
|
|
40
|
+
asVoice?: boolean;
|
|
41
|
+
audioAsVoice?: boolean;
|
|
40
42
|
mediaLocalRoots?: readonly string[];
|
|
41
43
|
resolveVerifiedTarget: (to: string, accountId: string) => { sessionKey: string; route: any; displayScope: string };
|
|
42
44
|
rememberSessionRoute: (sessionKey: string, accountId: string, route: any) => void;
|
|
@@ -44,7 +46,7 @@ export async function sendBncrMedia(params: {
|
|
|
44
46
|
accountId: string;
|
|
45
47
|
sessionKey: string;
|
|
46
48
|
route: any;
|
|
47
|
-
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] };
|
|
49
|
+
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[]; asVoice?: boolean; audioAsVoice?: boolean };
|
|
48
50
|
mediaLocalRoots?: readonly string[];
|
|
49
51
|
}) => Promise<void>;
|
|
50
52
|
createMessageId: () => string;
|
|
@@ -59,6 +61,8 @@ export async function sendBncrMedia(params: {
|
|
|
59
61
|
payload: {
|
|
60
62
|
text: params.text || '',
|
|
61
63
|
mediaUrl: params.mediaUrl || '',
|
|
64
|
+
asVoice: params.asVoice === true ? true : undefined,
|
|
65
|
+
audioAsVoice: params.audioAsVoice === true ? true : undefined,
|
|
62
66
|
},
|
|
63
67
|
mediaLocalRoots: params.mediaLocalRoots,
|
|
64
68
|
});
|