@xmoxmo/bncr 0.2.6 → 0.2.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 +7 -1
- package/index.ts +30 -15
- package/package.json +4 -3
- package/scripts/check-pack.mjs +61 -0
- package/scripts/selfcheck.mjs +10 -0
- package/src/channel.ts +130 -46
- package/src/core/targets.ts +10 -1
- package/src/messaging/inbound/commands.ts +20 -10
- package/src/messaging/inbound/context-facts.ts +200 -0
- package/src/messaging/inbound/dispatch.ts +66 -14
- package/src/messaging/inbound/gate.ts +66 -26
- package/src/messaging/inbound/runtime-compat.ts +39 -0
- package/src/messaging/inbound/session-label.ts +7 -7
- package/src/messaging/outbound/durable-message-adapter.ts +107 -0
- package/src/messaging/outbound/durable-queue-adapter.ts +157 -0
- package/src/messaging/outbound/session-route.ts +2 -2
- package/src/openclaw/config-runtime.ts +52 -0
- package/src/openclaw/inbound-session-runtime.ts +94 -0
- package/src/openclaw/ingress-runtime.ts +35 -0
- package/src/openclaw/media-runtime.ts +73 -0
- package/src/openclaw/reply-runtime.ts +104 -0
- package/src/openclaw/routing-runtime.ts +48 -0
- package/src/openclaw/sdk-helpers.ts +20 -0
- package/src/openclaw/session-route-runtime.ts +15 -0
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ openclaw plugins update bncr
|
|
|
34
34
|
openclaw gateway restart
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
> 兼容范围:`openclaw >= 2026.5.
|
|
37
|
+
> 兼容范围:`openclaw >= 2026.5.27`
|
|
38
38
|
>
|
|
39
39
|
> 如果你是从精确版本升级,或本地安装记录仍钉在旧版本,也可以显式执行:
|
|
40
40
|
>
|
|
@@ -95,6 +95,10 @@ bncr 当前采用两层模型:
|
|
|
95
95
|
- 在 OpenClaw 内部按正式 `channel plugin` 建模
|
|
96
96
|
- 负责入站解析、消息分发、出站适配、状态与治理
|
|
97
97
|
|
|
98
|
+
出站可靠投递的边界:bncr 后面仍有自己的服务框架和 outbox / ACK / retry / deadLetter 体系。对 OpenClaw 宿主来说,消息成功交给 bncr 插件并进入 bncr 自管 outbox,即表示频道 handoff 完成;这不等价于客户端 ACK 或目标平台最终送达。后续可靠投递由 bncr 自身负责。
|
|
99
|
+
|
|
100
|
+
当前已注册生产 `channel.message` 作为 bncr 的频道专用 handoff adapter:`text` / `media` / `payload` 会转换为 bncr outbox entry。原有通用 `message.send` / `channel.actions.send` 发送能力继续保留;`channel.message` 是频道专用入口,不替代通用发送入口。该 adapter 仍不启用 `durableFinal`;进入 outbox 后的客户端 ACK、目标平台送达、retry、deadLetter 继续由 bncr 服务框架负责。
|
|
101
|
+
|
|
98
102
|
当前代码结构:
|
|
99
103
|
|
|
100
104
|
```text
|
|
@@ -246,6 +250,7 @@ npm root -g
|
|
|
246
250
|
cd plugins/bncr
|
|
247
251
|
npm test
|
|
248
252
|
npm run selfcheck
|
|
253
|
+
npm run check-pack
|
|
249
254
|
npm pack
|
|
250
255
|
```
|
|
251
256
|
|
|
@@ -253,6 +258,7 @@ npm pack
|
|
|
253
258
|
|
|
254
259
|
- `npm test`:跑回归测试
|
|
255
260
|
- `npm run selfcheck`:检查插件骨架是否完整
|
|
261
|
+
- `npm run check-pack`:执行 `npm pack --dry-run --json`,确认发布包包含关键入口与 OpenClaw adapter 文件
|
|
256
262
|
- `npm pack`:确认当前版本可正常打包
|
|
257
263
|
- `npm run check-register-drift -- --duration-sec 300 --interval-sec 15`:静置采样 `bncr.diagnostics`,观察 `registerCount / apiGeneration / postWarmupRegisterCount` 是否在 warmup 后继续增长
|
|
258
264
|
|
package/index.ts
CHANGED
|
@@ -5,6 +5,10 @@ import path from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { BncrConfigSchema } from './src/core/config-schema.ts';
|
|
7
7
|
import { emitBncrLogLine } from './src/core/logging.ts';
|
|
8
|
+
import {
|
|
9
|
+
getOpenClawRuntimeConfig,
|
|
10
|
+
mutateOpenClawRuntimeConfigFile,
|
|
11
|
+
} from './src/openclaw/config-runtime.ts';
|
|
8
12
|
|
|
9
13
|
const pluginFile = fileURLToPath(import.meta.url);
|
|
10
14
|
const pluginDir = path.dirname(pluginFile);
|
|
@@ -660,35 +664,46 @@ const registerBncrCli = (api: OpenClawPluginApi & { registerCli?: (...args: any[
|
|
|
660
664
|
'Seed minimal channels.bncr config (adds enabled=true and allowTool=false only when missing)',
|
|
661
665
|
)
|
|
662
666
|
.action(async () => {
|
|
663
|
-
const cfg = api
|
|
664
|
-
const
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
const existing = isPlainObject(next.channels.bncr) ? next.channels.bncr : {};
|
|
668
|
-
const bncrCfg: Record<string, unknown> = { ...existing };
|
|
667
|
+
const cfg = getOpenClawRuntimeConfig(api) as Record<string, unknown>;
|
|
668
|
+
const channels = isPlainObject(cfg.channels) ? cfg.channels : {};
|
|
669
|
+
const existing = isPlainObject(channels.bncr) ? channels.bncr : {};
|
|
669
670
|
const added: string[] = [];
|
|
670
671
|
|
|
671
|
-
if (
|
|
672
|
-
bncrCfg.enabled = true;
|
|
672
|
+
if (existing.enabled === undefined) {
|
|
673
673
|
added.push('enabled=true');
|
|
674
674
|
}
|
|
675
675
|
|
|
676
|
-
if (
|
|
677
|
-
bncrCfg.allowTool = false;
|
|
676
|
+
if (existing.allowTool === undefined) {
|
|
678
677
|
added.push('allowTool=false');
|
|
679
678
|
}
|
|
680
679
|
|
|
681
|
-
next.channels.bncr = bncrCfg;
|
|
682
|
-
|
|
683
680
|
if (added.length === 0) {
|
|
684
681
|
console.log('Minimal bncr config already present. No changes made.');
|
|
685
682
|
return;
|
|
686
683
|
}
|
|
687
684
|
|
|
688
|
-
await api
|
|
685
|
+
await mutateOpenClawRuntimeConfigFile(api, {
|
|
686
|
+
afterWrite: { mode: 'auto' },
|
|
687
|
+
mutate(draft: Record<string, unknown>) {
|
|
688
|
+
if (!isPlainObject(draft.channels)) draft.channels = {};
|
|
689
|
+
const draftChannels = draft.channels as Record<string, unknown>;
|
|
690
|
+
const draftExisting = isPlainObject(draftChannels.bncr) ? draftChannels.bncr : {};
|
|
691
|
+
const draftBncrCfg: Record<string, unknown> = { ...draftExisting };
|
|
692
|
+
|
|
693
|
+
if (draftBncrCfg.enabled === undefined) {
|
|
694
|
+
draftBncrCfg.enabled = true;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
if (draftBncrCfg.allowTool === undefined) {
|
|
698
|
+
draftBncrCfg.allowTool = false;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
draftChannels.bncr = draftBncrCfg;
|
|
702
|
+
},
|
|
703
|
+
});
|
|
689
704
|
console.log('Seeded minimal bncr config at channels.bncr.');
|
|
690
705
|
console.log(`Added missing fields: ${added.join(', ')}`);
|
|
691
|
-
console.log('
|
|
706
|
+
console.log('Gateway will apply the config using the host afterWrite policy.');
|
|
692
707
|
});
|
|
693
708
|
},
|
|
694
709
|
{ commands: ['bncr'] },
|
|
@@ -801,7 +816,7 @@ const plugin = {
|
|
|
801
816
|
|
|
802
817
|
const resolveDebug = async () => {
|
|
803
818
|
try {
|
|
804
|
-
const cfg = api
|
|
819
|
+
const cfg = getOpenClawRuntimeConfig(api);
|
|
805
820
|
return Boolean((cfg as any)?.channels?.bncr?.debug?.verbose);
|
|
806
821
|
} catch {
|
|
807
822
|
return false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@xmoxmo/bncr",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,17 +27,18 @@
|
|
|
27
27
|
"selfcheck": "node ./scripts/selfcheck.mjs",
|
|
28
28
|
"test": "node --import ./tests/register-ts-hooks.mjs --test ./tests/*.test.mjs",
|
|
29
29
|
"check-register-drift": "node ./scripts/check-register-drift.mjs",
|
|
30
|
+
"check-pack": "node ./scripts/check-pack.mjs",
|
|
30
31
|
"format:check": "biome format --check .",
|
|
31
32
|
"format": "biome format --write .",
|
|
32
33
|
"lint": "biome lint .",
|
|
33
34
|
"check": "biome check ."
|
|
34
35
|
},
|
|
35
36
|
"peerDependencies": {
|
|
36
|
-
"openclaw": ">=2026.5.
|
|
37
|
+
"openclaw": ">=2026.5.27"
|
|
37
38
|
},
|
|
38
39
|
"devDependencies": {
|
|
39
40
|
"@biomejs/biome": "^1.9.4",
|
|
40
|
-
"openclaw": ">=2026.5.
|
|
41
|
+
"openclaw": ">=2026.5.27"
|
|
41
42
|
},
|
|
42
43
|
"openclaw": {
|
|
43
44
|
"extensions": [
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
7
|
+
const __dirname = path.dirname(__filename);
|
|
8
|
+
const root = path.resolve(__dirname, '..');
|
|
9
|
+
|
|
10
|
+
const requiredPackFiles = [
|
|
11
|
+
'README.md',
|
|
12
|
+
'index.ts',
|
|
13
|
+
'openclaw.plugin.json',
|
|
14
|
+
'package.json',
|
|
15
|
+
'scripts/selfcheck.mjs',
|
|
16
|
+
'scripts/check-register-drift.mjs',
|
|
17
|
+
'src/channel.ts',
|
|
18
|
+
'src/messaging/outbound/durable-message-adapter.ts',
|
|
19
|
+
'src/messaging/outbound/durable-queue-adapter.ts',
|
|
20
|
+
'src/openclaw/config-runtime.ts',
|
|
21
|
+
'src/openclaw/inbound-session-runtime.ts',
|
|
22
|
+
'src/openclaw/ingress-runtime.ts',
|
|
23
|
+
'src/openclaw/media-runtime.ts',
|
|
24
|
+
'src/openclaw/reply-runtime.ts',
|
|
25
|
+
'src/openclaw/routing-runtime.ts',
|
|
26
|
+
'src/openclaw/sdk-helpers.ts',
|
|
27
|
+
'src/openclaw/session-route-runtime.ts',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
const pkg = JSON.parse(fs.readFileSync(path.join(root, 'package.json'), 'utf8'));
|
|
31
|
+
const output = execFileSync('npm', ['pack', '--dry-run', '--json'], {
|
|
32
|
+
cwd: root,
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
35
|
+
});
|
|
36
|
+
const [pack] = JSON.parse(output);
|
|
37
|
+
const packedFiles = new Set((pack?.files ?? []).map((file) => file.path));
|
|
38
|
+
const missing = requiredPackFiles.filter((file) => !packedFiles.has(file));
|
|
39
|
+
const channelSource = fs.readFileSync(path.join(root, 'src/channel.ts'), 'utf8');
|
|
40
|
+
const channelMessageChecks = {
|
|
41
|
+
registered: channelSource.includes('message: {'),
|
|
42
|
+
text: channelSource.includes('channelMessageSendText'),
|
|
43
|
+
media: channelSource.includes('channelMessageSendMedia'),
|
|
44
|
+
payload: channelSource.includes('channelMessageSendPayload'),
|
|
45
|
+
manualAck: channelSource.includes("defaultAckPolicy: 'manual'"),
|
|
46
|
+
genericActionsPreserved: channelSource.includes('actions: messageActions'),
|
|
47
|
+
noDurableFinal: !channelSource.includes('durableFinal:'),
|
|
48
|
+
};
|
|
49
|
+
const channelMessageOk = Object.values(channelMessageChecks).every(Boolean);
|
|
50
|
+
|
|
51
|
+
const result = {
|
|
52
|
+
ok: missing.length === 0 && pkg.peerDependencies?.openclaw === '>=2026.5.27' && channelMessageOk,
|
|
53
|
+
package: pack?.id,
|
|
54
|
+
entryCount: pack?.entryCount,
|
|
55
|
+
missing,
|
|
56
|
+
openclaw: pkg.peerDependencies?.openclaw,
|
|
57
|
+
channelMessageChecks,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
console.log(JSON.stringify(result, null, 2));
|
|
61
|
+
if (!result.ok) process.exit(1);
|
package/scripts/selfcheck.mjs
CHANGED
|
@@ -24,6 +24,16 @@ const requiredFiles = [
|
|
|
24
24
|
'src/messaging/outbound/send.ts',
|
|
25
25
|
'src/messaging/outbound/media.ts',
|
|
26
26
|
'src/messaging/outbound/actions.ts',
|
|
27
|
+
'src/messaging/outbound/durable-message-adapter.ts',
|
|
28
|
+
'src/messaging/outbound/durable-queue-adapter.ts',
|
|
29
|
+
'src/openclaw/config-runtime.ts',
|
|
30
|
+
'src/openclaw/inbound-session-runtime.ts',
|
|
31
|
+
'src/openclaw/ingress-runtime.ts',
|
|
32
|
+
'src/openclaw/media-runtime.ts',
|
|
33
|
+
'src/openclaw/reply-runtime.ts',
|
|
34
|
+
'src/openclaw/routing-runtime.ts',
|
|
35
|
+
'src/openclaw/sdk-helpers.ts',
|
|
36
|
+
'src/openclaw/session-route-runtime.ts',
|
|
27
37
|
];
|
|
28
38
|
|
|
29
39
|
const readPackageVersion = () => {
|
package/src/channel.ts
CHANGED
|
@@ -1,21 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import { readBooleanParam } from 'openclaw/plugin-sdk/boolean-param';
|
|
5
4
|
import type {
|
|
6
5
|
GatewayRequestHandlerOptions,
|
|
7
6
|
OpenClawPluginApi,
|
|
8
7
|
OpenClawPluginServiceContext,
|
|
9
8
|
} from 'openclaw/plugin-sdk/core';
|
|
10
|
-
import {
|
|
11
|
-
applyAccountNameToChannelSection,
|
|
12
|
-
jsonResult,
|
|
13
|
-
setAccountEnabledInConfigSection,
|
|
14
|
-
} from 'openclaw/plugin-sdk/core';
|
|
15
|
-
import { readJsonFileWithFallback, writeJsonFileAtomically } from 'openclaw/plugin-sdk/json-store';
|
|
16
|
-
import { readStringParam } from 'openclaw/plugin-sdk/param-readers';
|
|
17
|
-
import { createDefaultChannelRuntimeState } from 'openclaw/plugin-sdk/status-helpers';
|
|
18
|
-
import { extractToolSend } from 'openclaw/plugin-sdk/tool-send';
|
|
19
9
|
import {
|
|
20
10
|
BNCR_DEFAULT_ACCOUNT_ID,
|
|
21
11
|
CHANNEL_ID,
|
|
@@ -78,6 +68,27 @@ import { buildExtendedDiagnostics as buildExtendedDiagnosticsFromRuntime } from
|
|
|
78
68
|
import { observeLeaseState, matchesTransferOwner as matchesTransferOwnerFromRuntime } from './core/lease-state.ts';
|
|
79
69
|
import { emitBncrLog, emitBncrLogLine } from './core/logging.ts';
|
|
80
70
|
import { resolveBncrChannelPolicy, resolveBncrConfigWarnings } from './core/policy.ts';
|
|
71
|
+
import {
|
|
72
|
+
getOpenClawRuntimeConfig,
|
|
73
|
+
getOpenClawRuntimeConfigOrDefault,
|
|
74
|
+
} from './openclaw/config-runtime.ts';
|
|
75
|
+
import {
|
|
76
|
+
loadOpenClawWebMedia,
|
|
77
|
+
saveOpenClawChannelMediaBuffer,
|
|
78
|
+
type OpenClawLoadedMedia,
|
|
79
|
+
} from './openclaw/media-runtime.ts';
|
|
80
|
+
import { resolveOpenClawAgentRoute } from './openclaw/routing-runtime.ts';
|
|
81
|
+
import {
|
|
82
|
+
applyOpenClawAccountNameToChannelSection,
|
|
83
|
+
createOpenClawDefaultChannelRuntimeState,
|
|
84
|
+
extractOpenClawToolSend,
|
|
85
|
+
openClawJsonResult,
|
|
86
|
+
readOpenClawBooleanParam,
|
|
87
|
+
readOpenClawJsonFileWithFallback,
|
|
88
|
+
readOpenClawStringParam,
|
|
89
|
+
setOpenClawAccountEnabledInConfigSection,
|
|
90
|
+
writeOpenClawJsonFileAtomically,
|
|
91
|
+
} from './openclaw/sdk-helpers.ts';
|
|
81
92
|
import {
|
|
82
93
|
buildRegisterTraceSummary as buildRegisterTraceSummaryFromEntries,
|
|
83
94
|
classifyRegisterTrace as classifyRegisterTraceFromStack,
|
|
@@ -309,6 +320,7 @@ import {
|
|
|
309
320
|
computeRetryRerouteDecision,
|
|
310
321
|
} from './messaging/outbound/retry-policy.ts';
|
|
311
322
|
import { sendBncrMedia, sendBncrText } from './messaging/outbound/send.ts';
|
|
323
|
+
import { buildBncrDurableQueuedResult } from './messaging/outbound/durable-queue-adapter.ts';
|
|
312
324
|
import { resolveBncrOutboundSessionRoute } from './messaging/outbound/session-route.ts';
|
|
313
325
|
import {
|
|
314
326
|
looksLikeBncrExplicitTarget,
|
|
@@ -481,20 +493,20 @@ function normalizeBncrSendParams(input: {
|
|
|
481
493
|
accountId: string;
|
|
482
494
|
}): NormalizedBncrSendParams {
|
|
483
495
|
const paramsObj = isPlainObject(input.params) ? input.params : {};
|
|
484
|
-
const to =
|
|
496
|
+
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
485
497
|
const resolvedAccountId = normalizeAccountId(
|
|
486
|
-
|
|
498
|
+
readOpenClawStringParam(paramsObj, 'accountId') ?? input.accountId,
|
|
487
499
|
);
|
|
488
500
|
|
|
489
|
-
const message =
|
|
490
|
-
const caption =
|
|
501
|
+
const message = readOpenClawStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
|
|
502
|
+
const caption = readOpenClawStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
|
|
491
503
|
const mediaUrl =
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
const asVoice =
|
|
497
|
-
const audioAsVoice =
|
|
504
|
+
readOpenClawStringParam(paramsObj, 'media', { trim: false }) ??
|
|
505
|
+
readOpenClawStringParam(paramsObj, 'path', { trim: false }) ??
|
|
506
|
+
readOpenClawStringParam(paramsObj, 'filePath', { trim: false }) ??
|
|
507
|
+
readOpenClawStringParam(paramsObj, 'mediaUrl', { trim: false });
|
|
508
|
+
const asVoice = readOpenClawBooleanParam(paramsObj, 'asVoice') ?? false;
|
|
509
|
+
const audioAsVoice = readOpenClawBooleanParam(paramsObj, 'audioAsVoice') ?? false;
|
|
498
510
|
|
|
499
511
|
if (asVoice && !mediaUrl) throw new Error('send voice requires media path');
|
|
500
512
|
|
|
@@ -1198,7 +1210,7 @@ class BncrBridgeRuntime {
|
|
|
1198
1210
|
this.stopped = false;
|
|
1199
1211
|
this.statePath = path.join(ctx.stateDir, 'bncr-bridge-state.json');
|
|
1200
1212
|
try {
|
|
1201
|
-
const cfg = this.api
|
|
1213
|
+
const cfg = getOpenClawRuntimeConfig(this.api);
|
|
1202
1214
|
this.initializeCanonicalAgentId(cfg);
|
|
1203
1215
|
for (const warning of resolveBncrConfigWarnings(cfg?.channels?.[CHANNEL_ID] || {})) {
|
|
1204
1216
|
this.logWarn('config', warning);
|
|
@@ -1292,7 +1304,7 @@ class BncrBridgeRuntime {
|
|
|
1292
1304
|
|
|
1293
1305
|
private async refreshDebugFlagFromConfig(options?: { forceLog?: boolean }) {
|
|
1294
1306
|
try {
|
|
1295
|
-
const cfg = this.api
|
|
1307
|
+
const cfg = getOpenClawRuntimeConfig(this.api);
|
|
1296
1308
|
const raw = (cfg as any)?.channels?.[CHANNEL_ID]?.debug?.verbose;
|
|
1297
1309
|
const next = typeof raw === 'boolean' ? raw : false;
|
|
1298
1310
|
const changed = next !== BNCR_DEBUG_VERBOSE;
|
|
@@ -1316,7 +1328,7 @@ class BncrBridgeRuntime {
|
|
|
1316
1328
|
channelId?: string;
|
|
1317
1329
|
}): string | null {
|
|
1318
1330
|
try {
|
|
1319
|
-
const resolved = this.api
|
|
1331
|
+
const resolved = resolveOpenClawAgentRoute(this.api, {
|
|
1320
1332
|
cfg: args.cfg,
|
|
1321
1333
|
channel: args.channelId || CHANNEL_ID,
|
|
1322
1334
|
accountId: normalizeAccountId(args.accountId),
|
|
@@ -1440,7 +1452,7 @@ class BncrBridgeRuntime {
|
|
|
1440
1452
|
|
|
1441
1453
|
private async loadState() {
|
|
1442
1454
|
if (!this.statePath) return;
|
|
1443
|
-
const loaded = await
|
|
1455
|
+
const loaded = await readOpenClawJsonFileWithFallback(this.statePath, {
|
|
1444
1456
|
outbox: [],
|
|
1445
1457
|
deadLetter: [],
|
|
1446
1458
|
sessionRoutes: [],
|
|
@@ -1709,7 +1721,7 @@ class BncrBridgeRuntime {
|
|
|
1709
1721
|
: null,
|
|
1710
1722
|
};
|
|
1711
1723
|
|
|
1712
|
-
await
|
|
1724
|
+
await writeOpenClawJsonFileAtomically(this.statePath, data);
|
|
1713
1725
|
}
|
|
1714
1726
|
|
|
1715
1727
|
private resolveMessageAck(messageId: string, result: 'acked' | 'timeout' = 'acked') {
|
|
@@ -2863,10 +2875,10 @@ class BncrBridgeRuntime {
|
|
|
2863
2875
|
});
|
|
2864
2876
|
}
|
|
2865
2877
|
|
|
2866
|
-
private prepareInboundAcceptance(args: {
|
|
2878
|
+
private async prepareInboundAcceptance(args: {
|
|
2867
2879
|
parsed: ReturnType<typeof parseBncrInboundParams>;
|
|
2868
2880
|
canonicalAgentId: string;
|
|
2869
|
-
}):
|
|
2881
|
+
}): Promise<
|
|
2870
2882
|
| {
|
|
2871
2883
|
ok: true;
|
|
2872
2884
|
accountId: string;
|
|
@@ -2878,7 +2890,8 @@ class BncrBridgeRuntime {
|
|
|
2878
2890
|
ok: false;
|
|
2879
2891
|
status: boolean;
|
|
2880
2892
|
payload: ReturnType<typeof buildInboundResponsePayload>;
|
|
2881
|
-
}
|
|
2893
|
+
}
|
|
2894
|
+
> {
|
|
2882
2895
|
const { parsed, canonicalAgentId } = args;
|
|
2883
2896
|
const {
|
|
2884
2897
|
accountId,
|
|
@@ -2915,8 +2928,8 @@ class BncrBridgeRuntime {
|
|
|
2915
2928
|
};
|
|
2916
2929
|
}
|
|
2917
2930
|
|
|
2918
|
-
const cfg = this.api
|
|
2919
|
-
const gate = checkBncrMessageGate({
|
|
2931
|
+
const cfg = getOpenClawRuntimeConfig(this.api);
|
|
2932
|
+
const gate = await checkBncrMessageGate({
|
|
2920
2933
|
parsed,
|
|
2921
2934
|
cfg,
|
|
2922
2935
|
account: resolveAccount(cfg, accountId),
|
|
@@ -2944,7 +2957,7 @@ class BncrBridgeRuntime {
|
|
|
2944
2957
|
taskKey: extracted.taskKey,
|
|
2945
2958
|
text,
|
|
2946
2959
|
extractedText: extracted.text,
|
|
2947
|
-
resolveAgentRoute: (params) => this.api
|
|
2960
|
+
resolveAgentRoute: (params) => resolveOpenClawAgentRoute(this.api, params),
|
|
2948
2961
|
});
|
|
2949
2962
|
|
|
2950
2963
|
return {
|
|
@@ -3089,7 +3102,7 @@ class BncrBridgeRuntime {
|
|
|
3089
3102
|
|
|
3090
3103
|
private isOutboundAckRequired(accountId?: string) {
|
|
3091
3104
|
try {
|
|
3092
|
-
const cfg = this.api
|
|
3105
|
+
const cfg = getOpenClawRuntimeConfig(this.api);
|
|
3093
3106
|
const channelCfg = (cfg as any)?.channels?.[CHANNEL_ID];
|
|
3094
3107
|
const accountCfg =
|
|
3095
3108
|
accountId && channelCfg?.accounts && typeof channelCfg.accounts === 'object'
|
|
@@ -3108,7 +3121,7 @@ class BncrBridgeRuntime {
|
|
|
3108
3121
|
private buildRuntimeFlags(accountId?: string) {
|
|
3109
3122
|
let ackPolicySource: 'channel' | 'default' = 'default';
|
|
3110
3123
|
try {
|
|
3111
|
-
const cfg = this.api
|
|
3124
|
+
const cfg = getOpenClawRuntimeConfig(this.api);
|
|
3112
3125
|
const global = (cfg as any)?.channels?.[CHANNEL_ID]?.outboundRequireAck;
|
|
3113
3126
|
if (typeof global === 'boolean') ackPolicySource = 'channel';
|
|
3114
3127
|
} catch {
|
|
@@ -3920,7 +3933,7 @@ class BncrBridgeRuntime {
|
|
|
3920
3933
|
const canonicalAgentId =
|
|
3921
3934
|
this.canonicalAgentId ||
|
|
3922
3935
|
this.ensureCanonicalAgentId({
|
|
3923
|
-
cfg: this.api
|
|
3936
|
+
cfg: getOpenClawRuntimeConfigOrDefault(this.api, {}),
|
|
3924
3937
|
accountId: acc,
|
|
3925
3938
|
channelId: CHANNEL_ID,
|
|
3926
3939
|
peer: { kind: 'direct', id: route.groupId === '0' ? route.userId : route.groupId },
|
|
@@ -4568,7 +4581,7 @@ class BncrBridgeRuntime {
|
|
|
4568
4581
|
mediaUrl: string,
|
|
4569
4582
|
mediaLocalRoots?: readonly string[],
|
|
4570
4583
|
): Promise<{ mediaBase64: string; mimeType?: string; fileName?: string }> {
|
|
4571
|
-
const loaded = await this.api
|
|
4584
|
+
const loaded = await loadOpenClawWebMedia(this.api, mediaUrl, {
|
|
4572
4585
|
localRoots: mediaLocalRoots,
|
|
4573
4586
|
maxBytes: 20 * 1024 * 1024,
|
|
4574
4587
|
});
|
|
@@ -4583,12 +4596,12 @@ class BncrBridgeRuntime {
|
|
|
4583
4596
|
mediaUrl: string;
|
|
4584
4597
|
mediaLocalRoots?: readonly string[];
|
|
4585
4598
|
}): Promise<{
|
|
4586
|
-
loaded:
|
|
4599
|
+
loaded: OpenClawLoadedMedia;
|
|
4587
4600
|
size: number;
|
|
4588
4601
|
mimeType?: string;
|
|
4589
4602
|
fileName: string;
|
|
4590
4603
|
}> {
|
|
4591
|
-
const loaded = await this.api
|
|
4604
|
+
const loaded = await loadOpenClawWebMedia(this.api, params.mediaUrl, {
|
|
4592
4605
|
localRoots: params.mediaLocalRoots,
|
|
4593
4606
|
maxBytes: 50 * 1024 * 1024,
|
|
4594
4607
|
});
|
|
@@ -5408,7 +5421,7 @@ class BncrBridgeRuntime {
|
|
|
5408
5421
|
|
|
5409
5422
|
handleDiagnostics = async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
5410
5423
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
5411
|
-
const cfg = this.api
|
|
5424
|
+
const cfg = getOpenClawRuntimeConfig(this.api);
|
|
5412
5425
|
const runtime = this.getAccountRuntimeSnapshot(accountId);
|
|
5413
5426
|
const diagnostics = this.buildExtendedDiagnostics(accountId);
|
|
5414
5427
|
|
|
@@ -5703,7 +5716,8 @@ class BncrBridgeRuntime {
|
|
|
5703
5716
|
throw new Error('file sha256 mismatch');
|
|
5704
5717
|
}
|
|
5705
5718
|
|
|
5706
|
-
const saved = await
|
|
5719
|
+
const saved = await saveOpenClawChannelMediaBuffer(
|
|
5720
|
+
this.api,
|
|
5707
5721
|
merged,
|
|
5708
5722
|
st.mimeType,
|
|
5709
5723
|
'inbound',
|
|
@@ -6038,14 +6052,14 @@ class BncrBridgeRuntime {
|
|
|
6038
6052
|
this.lastInboundAtGlobal = now();
|
|
6039
6053
|
this.incrementCounter(this.inboundEventsByAccount, accountId);
|
|
6040
6054
|
|
|
6041
|
-
const cfg = this.api
|
|
6055
|
+
const cfg = getOpenClawRuntimeConfig(this.api);
|
|
6042
6056
|
const canonicalAgentId = this.ensureCanonicalAgentId({
|
|
6043
6057
|
cfg,
|
|
6044
6058
|
accountId,
|
|
6045
6059
|
peer,
|
|
6046
6060
|
channelId: CHANNEL_ID,
|
|
6047
6061
|
});
|
|
6048
|
-
const acceptance = this.prepareInboundAcceptance({ parsed, canonicalAgentId });
|
|
6062
|
+
const acceptance = await this.prepareInboundAcceptance({ parsed, canonicalAgentId });
|
|
6049
6063
|
if (!acceptance.ok) {
|
|
6050
6064
|
respond(acceptance.status, acceptance.payload);
|
|
6051
6065
|
return;
|
|
@@ -6352,6 +6366,65 @@ class BncrBridgeRuntime {
|
|
|
6352
6366
|
createMessageId: () => randomUUID(),
|
|
6353
6367
|
});
|
|
6354
6368
|
};
|
|
6369
|
+
|
|
6370
|
+
private async enqueueChannelMessageHandoff(ctx: any, payload: ReplyPayloadInput) {
|
|
6371
|
+
const accountId = normalizeAccountId(ctx.accountId);
|
|
6372
|
+
const to = asString(ctx.to || '').trim();
|
|
6373
|
+
const verified = this.resolveVerifiedTarget(to, accountId);
|
|
6374
|
+
this.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
|
|
6375
|
+
const before = new Set(this.outbox.keys());
|
|
6376
|
+
await this.enqueueFromReply({
|
|
6377
|
+
accountId,
|
|
6378
|
+
sessionKey: verified.sessionKey,
|
|
6379
|
+
route: verified.route,
|
|
6380
|
+
payload,
|
|
6381
|
+
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
6382
|
+
});
|
|
6383
|
+
const entries = Array.from(this.outbox.values()).filter((entry) => !before.has(entry.messageId));
|
|
6384
|
+
if (!entries.length) {
|
|
6385
|
+
throw new Error('bncr channel.message handoff did not enqueue an outbox entry');
|
|
6386
|
+
}
|
|
6387
|
+
return entries[entries.length - 1];
|
|
6388
|
+
}
|
|
6389
|
+
|
|
6390
|
+
channelMessageSendText = async (ctx: any) => {
|
|
6391
|
+
const entry = await this.enqueueChannelMessageHandoff(ctx, {
|
|
6392
|
+
text: asString(ctx.text || ''),
|
|
6393
|
+
kind: ctx?.kind,
|
|
6394
|
+
replyToId: this.resolveChannelSendReplyToId(ctx),
|
|
6395
|
+
});
|
|
6396
|
+
return buildBncrDurableQueuedResult({ entry });
|
|
6397
|
+
};
|
|
6398
|
+
|
|
6399
|
+
channelMessageSendMedia = async (ctx: any) => {
|
|
6400
|
+
const entry = await this.enqueueChannelMessageHandoff(ctx, {
|
|
6401
|
+
text: asString(ctx.text || ''),
|
|
6402
|
+
mediaUrl: asString(ctx.mediaUrl || ''),
|
|
6403
|
+
mediaUrls: Array.isArray(ctx?.mediaUrls) ? ctx.mediaUrls : undefined,
|
|
6404
|
+
asVoice: ctx?.asVoice === true,
|
|
6405
|
+
audioAsVoice: ctx?.audioAsVoice === true,
|
|
6406
|
+
kind: ctx?.kind,
|
|
6407
|
+
replyToId: this.resolveChannelSendReplyToId(ctx),
|
|
6408
|
+
});
|
|
6409
|
+
return buildBncrDurableQueuedResult({ entry });
|
|
6410
|
+
};
|
|
6411
|
+
|
|
6412
|
+
channelMessageSendPayload = async (ctx: any) => {
|
|
6413
|
+
const payload = ctx?.payload || {};
|
|
6414
|
+
if (!payload || typeof payload !== 'object') {
|
|
6415
|
+
throw new Error('bncr channel.message payload must be an object');
|
|
6416
|
+
}
|
|
6417
|
+
const entry = await this.enqueueChannelMessageHandoff(ctx, {
|
|
6418
|
+
text: asString(payload.text || payload.message || payload.caption || ''),
|
|
6419
|
+
mediaUrl: asString(payload.mediaUrl || ''),
|
|
6420
|
+
mediaUrls: Array.isArray(payload.mediaUrls) ? payload.mediaUrls : undefined,
|
|
6421
|
+
asVoice: payload.asVoice === true,
|
|
6422
|
+
audioAsVoice: payload.audioAsVoice === true,
|
|
6423
|
+
kind: payload.kind,
|
|
6424
|
+
replyToId: asString(payload.replyToId || ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
6425
|
+
});
|
|
6426
|
+
return buildBncrDurableQueuedResult({ entry });
|
|
6427
|
+
};
|
|
6355
6428
|
}
|
|
6356
6429
|
|
|
6357
6430
|
export function createBncrBridge(api: OpenClawPluginApi) {
|
|
@@ -6387,7 +6460,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
6387
6460
|
};
|
|
6388
6461
|
},
|
|
6389
6462
|
supportsAction: ({ action }) => action === 'send',
|
|
6390
|
-
extractToolSend: ({ args })
|
|
6463
|
+
extractToolSend: ({ args }) => extractOpenClawToolSend(args, 'sendMessage'),
|
|
6391
6464
|
handleAction: async ({ action, params, accountId, mediaLocalRoots }) => {
|
|
6392
6465
|
if (action !== 'send')
|
|
6393
6466
|
throw new Error(`Action ${action} is not supported for provider ${CHANNEL_ID}.`);
|
|
@@ -6425,7 +6498,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
6425
6498
|
createMessageId: () => randomUUID(),
|
|
6426
6499
|
});
|
|
6427
6500
|
|
|
6428
|
-
return
|
|
6501
|
+
return openClawJsonResult({ ok: true, ...result });
|
|
6429
6502
|
},
|
|
6430
6503
|
};
|
|
6431
6504
|
|
|
@@ -6440,6 +6513,17 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
6440
6513
|
aliases: ['bncr'],
|
|
6441
6514
|
},
|
|
6442
6515
|
actions: messageActions,
|
|
6516
|
+
message: {
|
|
6517
|
+
receive: {
|
|
6518
|
+
defaultAckPolicy: 'manual' as const,
|
|
6519
|
+
supportedAckPolicies: ['manual'] as const,
|
|
6520
|
+
},
|
|
6521
|
+
send: {
|
|
6522
|
+
text: async (ctx: any) => getBridge().channelMessageSendText(ctx),
|
|
6523
|
+
media: async (ctx: any) => getBridge().channelMessageSendMedia(ctx),
|
|
6524
|
+
payload: async (ctx: any) => getBridge().channelMessageSendPayload(ctx),
|
|
6525
|
+
},
|
|
6526
|
+
},
|
|
6443
6527
|
capabilities: {
|
|
6444
6528
|
chatTypes: ['direct'] as ChatType[],
|
|
6445
6529
|
media: true,
|
|
@@ -6528,7 +6612,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
6528
6612
|
listAccountIds,
|
|
6529
6613
|
resolveAccount,
|
|
6530
6614
|
setAccountEnabled: ({ cfg, accountId, enabled }: any) =>
|
|
6531
|
-
|
|
6615
|
+
setOpenClawAccountEnabledInConfigSection({
|
|
6532
6616
|
cfg,
|
|
6533
6617
|
sectionKey: CHANNEL_ID,
|
|
6534
6618
|
accountId,
|
|
@@ -6552,7 +6636,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
6552
6636
|
},
|
|
6553
6637
|
setup: {
|
|
6554
6638
|
applyAccountName: ({ cfg, accountId, name }: any) =>
|
|
6555
|
-
|
|
6639
|
+
applyOpenClawAccountNameToChannelSection({
|
|
6556
6640
|
cfg,
|
|
6557
6641
|
channelKey: CHANNEL_ID,
|
|
6558
6642
|
accountId,
|
|
@@ -6604,7 +6688,7 @@ export function createBncrChannelPlugin(getBridge: () => BncrBridgeRuntime) {
|
|
|
6604
6688
|
}),
|
|
6605
6689
|
},
|
|
6606
6690
|
status: {
|
|
6607
|
-
defaultRuntime:
|
|
6691
|
+
defaultRuntime: createOpenClawDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
|
|
6608
6692
|
mode: 'ws-offline',
|
|
6609
6693
|
}),
|
|
6610
6694
|
buildChannelSummary: async ({ defaultAccountId }: any) => {
|
package/src/core/targets.ts
CHANGED
|
@@ -51,8 +51,17 @@ function parseRouteFromStandardDisplayScope(scope: string): BncrRoute | null {
|
|
|
51
51
|
return null;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
function normalizeDisplayScopePrefix(scope: string): string {
|
|
55
55
|
const raw = asString(scope).trim();
|
|
56
|
+
if (!raw) return '';
|
|
57
|
+
if (raw.startsWith('Bncr:')) return raw;
|
|
58
|
+
if (/^bncr[:-]/i.test(raw)) return raw;
|
|
59
|
+
if (!parseRouteFromStandardDisplayScope(raw)) return raw;
|
|
60
|
+
return `Bncr:${raw}`;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
|
|
64
|
+
const raw = normalizeDisplayScopePrefix(scope);
|
|
56
65
|
if (!raw) return null;
|
|
57
66
|
|
|
58
67
|
const payload = raw.match(/^Bncr:(.+)$/)?.[1];
|