@xmoxmo/bncr 0.5.6 → 0.5.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/dist/index.js +1 -9
- package/index.ts +1 -9
- package/package.json +1 -1
- package/scripts/check-pack.mjs +0 -6
- package/scripts/selfcheck.mjs +0 -7
- package/src/channel.ts +79 -43
- package/src/core/accounts.ts +1 -6
- package/src/core/connection-reachability.ts +1 -5
- package/src/core/dead-letter-diagnostics.ts +4 -10
- package/src/core/outbox-entry-builders.ts +34 -18
- package/src/core/outbox-file-transfer-failure.ts +5 -0
- package/src/core/outbox-file-transfer-guards.ts +6 -0
- package/src/core/outbox-file-transfer-prep.ts +8 -5
- package/src/core/outbox-file-transfer-success.ts +5 -0
- package/src/core/outbox-queue.ts +1 -7
- package/src/core/outbox-summary.ts +15 -27
- package/src/core/outbox-text-push-prep.ts +1 -36
- package/src/core/permissions.ts +1 -9
- package/src/core/persisted-outbox-entry.ts +54 -11
- package/src/core/policy.ts +1 -11
- package/src/core/register-trace.ts +2 -5
- package/src/core/status-meta.ts +1 -9
- package/src/core/status.ts +1 -21
- package/src/core/targets.ts +52 -70
- package/src/core/types.ts +1 -17
- package/src/core/value-sanitize.ts +38 -1
- package/src/messaging/inbound/commands.ts +2 -24
- package/src/messaging/inbound/dispatch-prep.ts +1 -1
- package/src/messaging/inbound/gate.ts +2 -31
- package/src/messaging/inbound/group-history.ts +0 -75
- package/src/messaging/inbound/parse.ts +1 -31
- package/src/messaging/inbound/scene-admin.ts +8 -8
- package/src/messaging/outbound/diagnostics.ts +1 -6
- package/src/messaging/outbound/durable-queue-adapter.ts +17 -11
- package/src/messaging/outbound/marker-parser.ts +62 -28
- package/src/messaging/outbound/media-dedupe.ts +2 -6
- package/src/messaging/outbound/media.ts +1 -6
- package/src/messaging/outbound/normalize-outbound-send.ts +327 -0
- package/src/messaging/outbound/queue-selectors.ts +1 -5
- package/src/messaging/outbound/reply-enqueue.ts +121 -70
- package/src/messaging/outbound/session-route.ts +6 -11
- package/src/messaging/outbound/target-resolver.ts +1 -6
- package/src/openclaw/routing-runtime.ts +0 -11
- package/src/openclaw/sdk-helpers.ts +0 -2
- package/src/plugin/bridge-file-transfer-push-facade.ts +6 -6
- package/src/plugin/bridge-media-facade.ts +0 -1
- package/src/plugin/channel-components.ts +0 -13
- package/src/plugin/channel-plugin-surface-group.ts +52 -65
- package/src/plugin/channel-runtime-builders-delivery.ts +1 -0
- package/src/plugin/channel-runtime-types.ts +2 -24
- package/src/plugin/channel-send.ts +205 -221
- package/src/plugin/channel-utils.ts +8 -29
- package/src/plugin/connection-handlers-helpers.ts +0 -15
- package/src/plugin/connection-handlers.ts +6 -5
- package/src/plugin/file-inbound-abort.ts +5 -0
- package/src/plugin/file-inbound-chunk.ts +5 -0
- package/src/plugin/file-inbound-complete.ts +5 -0
- package/src/plugin/media-dedupe-runtime.ts +1 -6
- package/src/plugin/message-send.ts +7 -8
- package/src/plugin/outbound.ts +6 -26
- package/src/plugin/outbox-drain-loop.ts +2 -2
- package/src/plugin/outbox-file-push-flow.ts +7 -7
- package/src/plugin/outbox-text-push-flow.ts +1 -1
- package/src/plugin/target-runtime.ts +1 -6
- package/src/runtime/outbound-ack-timeout.ts +1 -8
- package/src/runtime/outbox-transitions.ts +1 -4
- package/src/runtime/status-worker.ts +1 -9
- package/src/messaging/outbound/build-send-action.ts +0 -130
- package/src/messaging/outbound/durable-message-adapter.ts +0 -121
- package/src/messaging/outbound/send-params.ts +0 -114
- package/src/messaging/outbound/send.ts +0 -127
- package/src/openclaw/ingress-runtime.ts +0 -25
- package/src/plugin/channel-send-runtime-group.ts +0 -37
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { asString } from '../core/value-sanitize.ts';
|
|
1
2
|
import {
|
|
2
3
|
buildMediaTextFallback,
|
|
3
4
|
type MediaDedupeCacheEntry,
|
|
@@ -5,12 +6,6 @@ import {
|
|
|
5
6
|
normalizeReplyToId,
|
|
6
7
|
} from '../messaging/outbound/media-dedupe.ts';
|
|
7
8
|
|
|
8
|
-
function asString(v: unknown, fallback = ''): string {
|
|
9
|
-
if (typeof v === 'string') return v;
|
|
10
|
-
if (v == null) return fallback;
|
|
11
|
-
return String(v);
|
|
12
|
-
}
|
|
13
|
-
|
|
14
9
|
export function createBncrMediaDedupeRuntime(runtime: {
|
|
15
10
|
now: () => number;
|
|
16
11
|
recentMediaDedupeBySession: Map<string, Map<string, MediaDedupeCacheEntry>>;
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
import type { ChannelMessageSendResult } from 'openclaw/plugin-sdk/channel-message';
|
|
2
|
-
|
|
3
|
-
type MessageSendContext = Record<string, unknown>;
|
|
2
|
+
import type { BncrChannelSendContext } from './channel-runtime-types.ts';
|
|
4
3
|
|
|
5
4
|
export type BncrMessageSendBridge = {
|
|
6
5
|
channelMessageSendText: (
|
|
7
|
-
ctx:
|
|
6
|
+
ctx: BncrChannelSendContext,
|
|
8
7
|
) => ChannelMessageSendResult | Promise<ChannelMessageSendResult>;
|
|
9
8
|
channelMessageSendMedia: (
|
|
10
|
-
ctx:
|
|
9
|
+
ctx: BncrChannelSendContext,
|
|
11
10
|
) => ChannelMessageSendResult | Promise<ChannelMessageSendResult>;
|
|
12
11
|
channelMessageSendPayload: (
|
|
13
|
-
ctx:
|
|
12
|
+
ctx: BncrChannelSendContext,
|
|
14
13
|
) => ChannelMessageSendResult | Promise<ChannelMessageSendResult>;
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
export function createBncrMessageSend(getBridge: () => BncrMessageSendBridge) {
|
|
18
17
|
return {
|
|
19
|
-
text: async (ctx:
|
|
20
|
-
media: async (ctx:
|
|
21
|
-
payload: async (ctx:
|
|
18
|
+
text: async (ctx: BncrChannelSendContext) => getBridge().channelMessageSendText(ctx),
|
|
19
|
+
media: async (ctx: BncrChannelSendContext) => getBridge().channelMessageSendMedia(ctx),
|
|
20
|
+
payload: async (ctx: BncrChannelSendContext) => getBridge().channelMessageSendPayload(ctx),
|
|
22
21
|
};
|
|
23
22
|
}
|
package/src/plugin/outbound.ts
CHANGED
|
@@ -1,35 +1,15 @@
|
|
|
1
1
|
import { normalizeAccountId } from '../core/accounts.ts';
|
|
2
|
+
import { asString } from '../core/value-sanitize.ts';
|
|
2
3
|
import {
|
|
3
4
|
deleteBncrMessageAction,
|
|
4
5
|
editBncrMessageAction,
|
|
5
6
|
reactBncrMessageAction,
|
|
6
7
|
sendBncrReplyAction,
|
|
7
8
|
} from '../messaging/outbound/actions.ts';
|
|
8
|
-
|
|
9
|
-
function asString(v: unknown, fallback = ''): string {
|
|
10
|
-
return typeof v === 'string' ? v : v == null ? fallback : String(v);
|
|
11
|
-
}
|
|
9
|
+
import type { BncrChannelSendContext } from './channel-runtime-types.ts';
|
|
12
10
|
|
|
13
11
|
type BncrOutboundDeliveryResult = { channel: string; messageId: string; chatId: string };
|
|
14
12
|
|
|
15
|
-
type BncrOutboundSendContext = {
|
|
16
|
-
accountId?: string | null;
|
|
17
|
-
to?: string;
|
|
18
|
-
text?: string;
|
|
19
|
-
mediaUrl?: string;
|
|
20
|
-
type?: string;
|
|
21
|
-
kind?: string;
|
|
22
|
-
replyToId?: string | null;
|
|
23
|
-
replyToMessageId?: string | null;
|
|
24
|
-
asVoice?: boolean;
|
|
25
|
-
audioAsVoice?: boolean;
|
|
26
|
-
mediaLocalRoots?: readonly string[];
|
|
27
|
-
/** Pass-through fields from host — bncr plugin does NOT consume these. */
|
|
28
|
-
forceDocument?: boolean;
|
|
29
|
-
gifPlayback?: boolean;
|
|
30
|
-
silent?: boolean;
|
|
31
|
-
};
|
|
32
|
-
|
|
33
13
|
type BncrOutboundReplyActionContext = {
|
|
34
14
|
accountId?: string | null;
|
|
35
15
|
to?: string;
|
|
@@ -54,18 +34,18 @@ type BncrOutboundEditActionContext = BncrOutboundTargetActionContext & {
|
|
|
54
34
|
|
|
55
35
|
export type BncrOutboundBridge = {
|
|
56
36
|
channelSendText: (
|
|
57
|
-
ctx:
|
|
37
|
+
ctx: BncrChannelSendContext,
|
|
58
38
|
) => BncrOutboundDeliveryResult | Promise<BncrOutboundDeliveryResult>;
|
|
59
39
|
channelSendMedia: (
|
|
60
|
-
ctx:
|
|
40
|
+
ctx: BncrChannelSendContext,
|
|
61
41
|
) => BncrOutboundDeliveryResult | Promise<BncrOutboundDeliveryResult>;
|
|
62
42
|
};
|
|
63
43
|
|
|
64
44
|
export function createBncrOutboundRuntime(getBridge: () => BncrOutboundBridge) {
|
|
65
45
|
return {
|
|
66
46
|
deliveryMode: 'gateway' as const,
|
|
67
|
-
sendText: async (ctx:
|
|
68
|
-
sendMedia: async (ctx:
|
|
47
|
+
sendText: async (ctx: BncrChannelSendContext) => getBridge().channelSendText(ctx),
|
|
48
|
+
sendMedia: async (ctx: BncrChannelSendContext) => getBridge().channelSendMedia(ctx),
|
|
69
49
|
replyAction: async (ctx: BncrOutboundReplyActionContext) =>
|
|
70
50
|
sendBncrReplyAction({
|
|
71
51
|
accountId: normalizeAccountId(ctx?.accountId),
|
|
@@ -295,8 +295,8 @@ export function createBncrOutboxDrainLoop(
|
|
|
295
295
|
try {
|
|
296
296
|
pushed = await runtime.tryPushEntry(entry);
|
|
297
297
|
} catch (error) {
|
|
298
|
-
const
|
|
299
|
-
if (
|
|
298
|
+
const msg = runtime.isPlainObject(entry.payload?.message) ? entry.payload.message : null;
|
|
299
|
+
if (msg?.transferMode === 'media') {
|
|
300
300
|
runtime.handleFileTransferPushFailure({ entry, error });
|
|
301
301
|
} else {
|
|
302
302
|
runtime.handleTextPushFailure({ entry, error });
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { resolveFileTransferGuard } from '../core/outbox-file-transfer-guards.ts';
|
|
2
|
-
import {
|
|
2
|
+
import { prepareRouteSelection } from '../core/outbox-file-transfer-prep.ts';
|
|
3
3
|
import type { BncrConnection, OutboxEntry } from '../core/types.ts';
|
|
4
4
|
import { selectOutboxFileTransferRouteCandidates } from '../messaging/outbound/queue-selectors.ts';
|
|
5
5
|
|
|
6
6
|
export async function runBncrFileTransferOutboxPush(args: {
|
|
7
7
|
entry: OutboxEntry;
|
|
8
|
-
|
|
8
|
+
msg: Record<string, unknown>;
|
|
9
9
|
gatewayContext: unknown;
|
|
10
10
|
owner: BncrConnection | null;
|
|
11
11
|
resolvePushConnIds: (accountId: string) => Set<string>;
|
|
@@ -18,7 +18,7 @@ export async function runBncrFileTransferOutboxPush(args: {
|
|
|
18
18
|
}) => void;
|
|
19
19
|
pushFileTransferSuccessPath: (args: {
|
|
20
20
|
entry: OutboxEntry;
|
|
21
|
-
|
|
21
|
+
msg: Record<string, unknown>;
|
|
22
22
|
owner: BncrConnection | null;
|
|
23
23
|
connIds: Iterable<string>;
|
|
24
24
|
recentInboundReachable: boolean;
|
|
@@ -27,21 +27,21 @@ export async function runBncrFileTransferOutboxPush(args: {
|
|
|
27
27
|
}) => Promise<void>;
|
|
28
28
|
handleFileTransferPushFailure: (args: { entry: OutboxEntry; error: unknown }) => void;
|
|
29
29
|
}) {
|
|
30
|
-
const selection =
|
|
30
|
+
const selection = prepareRouteSelection({
|
|
31
31
|
entry: args.entry,
|
|
32
32
|
owner: args.owner,
|
|
33
33
|
resolvePushConnIds: args.resolvePushConnIds,
|
|
34
34
|
resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
|
|
35
35
|
hasRecentInboundReachability: args.hasRecentInboundReachability,
|
|
36
36
|
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
37
|
-
selectOutboxFileTransferRouteCandidates,
|
|
37
|
+
selectRouteCandidates: selectOutboxFileTransferRouteCandidates,
|
|
38
38
|
});
|
|
39
39
|
const guard = resolveFileTransferGuard({
|
|
40
40
|
gatewayContext: args.gatewayContext,
|
|
41
41
|
entry: args.entry,
|
|
42
42
|
owner: args.owner,
|
|
43
43
|
routeSelection: selection,
|
|
44
|
-
mediaUrl: String(args.
|
|
44
|
+
mediaUrl: String(args.msg.mediaUrl || '').trim(),
|
|
45
45
|
});
|
|
46
46
|
if (!guard.ok) {
|
|
47
47
|
args.handleFileTransferPushGuardFailure({
|
|
@@ -54,7 +54,7 @@ export async function runBncrFileTransferOutboxPush(args: {
|
|
|
54
54
|
try {
|
|
55
55
|
await args.pushFileTransferSuccessPath({
|
|
56
56
|
entry: args.entry,
|
|
57
|
-
|
|
57
|
+
msg: args.msg,
|
|
58
58
|
owner: args.owner,
|
|
59
59
|
connIds: guard.connIds,
|
|
60
60
|
recentInboundReachable: guard.recentInboundReachable,
|
|
@@ -43,7 +43,7 @@ export async function runBncrTextOutboxPush(args: {
|
|
|
43
43
|
resolveRecentInboundConnIds: args.resolveRecentInboundConnIds,
|
|
44
44
|
hasRecentInboundReachability: args.hasRecentInboundReachability,
|
|
45
45
|
isRevalidatedAttemptedConn: args.isRevalidatedAttemptedConn,
|
|
46
|
-
selectOutboxRouteCandidates,
|
|
46
|
+
selectRouteCandidates: selectOutboxRouteCandidates,
|
|
47
47
|
});
|
|
48
48
|
const guard = resolveTextPushGuard({
|
|
49
49
|
gatewayContext: args.gatewayContext,
|
|
@@ -6,15 +6,10 @@ import {
|
|
|
6
6
|
routeKey,
|
|
7
7
|
} from '../core/targets.ts';
|
|
8
8
|
import type { BncrRoute } from '../core/types.ts';
|
|
9
|
+
import { asString } from '../core/value-sanitize.ts';
|
|
9
10
|
import { getOpenClawRuntimeConfigOrDefault } from '../openclaw/config-runtime.ts';
|
|
10
11
|
import type { BncrChannelConfigRoot } from './channel-runtime-types.ts';
|
|
11
12
|
|
|
12
|
-
function asString(v: unknown, fallback = ''): string {
|
|
13
|
-
if (typeof v === 'string') return v;
|
|
14
|
-
if (v == null) return fallback;
|
|
15
|
-
return String(v);
|
|
16
|
-
}
|
|
17
|
-
|
|
18
13
|
export function createBncrTargetRuntime(runtime: {
|
|
19
14
|
api: Parameters<typeof getOpenClawRuntimeConfigOrDefault>[0];
|
|
20
15
|
channelId: string;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BncrAckObservability, BncrAckStrategy } from '../core/types.ts';
|
|
2
|
+
import { asString, finiteNumberOr } from '../core/value-sanitize.ts';
|
|
2
3
|
|
|
3
4
|
type ComputeBncrRecommendedAckTimeoutArgs = {
|
|
4
5
|
lateAckOkCount: number;
|
|
@@ -76,14 +77,6 @@ export function resolveBncrRuntimeAckTimeoutDecision(args: ComputeBncrRecommende
|
|
|
76
77
|
return { timeoutMs, reason };
|
|
77
78
|
}
|
|
78
79
|
|
|
79
|
-
function finiteNumberOr(value: unknown, fallback: number) {
|
|
80
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function asString(value: unknown, fallback = '') {
|
|
84
|
-
return typeof value === 'string' ? value : fallback;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
80
|
export function buildBncrRuntimeAckStrategy(args: {
|
|
88
81
|
ackObservability: BncrAckObservability;
|
|
89
82
|
defaultAckTimeoutMs: number;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OutboxEntry } from '../core/types.ts';
|
|
2
|
+
import { finiteNumberOr } from '../core/value-sanitize.ts';
|
|
2
3
|
import type {
|
|
3
4
|
PushFailureDecision,
|
|
4
5
|
RetryRerouteDecision,
|
|
@@ -83,10 +84,6 @@ export function buildBncrOutboxPushSuccessEntryPatch(args: {
|
|
|
83
84
|
};
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
87
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
87
|
export function buildBncrAckOkTelemetryPatch(args: {
|
|
91
88
|
entry: OutboxEntry;
|
|
92
89
|
ackAt: number;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { normalizeAccountId } from '../core/accounts.ts';
|
|
2
2
|
import type { BncrDiagnosticsSummary } from '../core/types.ts';
|
|
3
|
+
import { finiteNumberOr, nonNegativeFiniteNumberOr } from '../core/value-sanitize.ts';
|
|
3
4
|
|
|
4
5
|
type StatusRuntimeMeta = {
|
|
5
6
|
pending?: number;
|
|
@@ -60,15 +61,6 @@ export function createHealthStatusLogState(): HealthStatusLogState {
|
|
|
60
61
|
return { emittedSig: null, pendingSig: null, pendingSince: 0 };
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
function finiteNumberOr(value: unknown, fallback: number): number {
|
|
64
|
-
const n = Number(value);
|
|
65
|
-
return Number.isFinite(n) ? n : fallback;
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function nonNegativeFiniteNumberOr(value: unknown, fallback: number): number {
|
|
69
|
-
return Math.max(0, finiteNumberOr(value, fallback));
|
|
70
|
-
}
|
|
71
|
-
|
|
72
64
|
export function updateHealthStatusLogState(args: {
|
|
73
65
|
state: HealthStatusLogState;
|
|
74
66
|
sig: string;
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
2
|
-
|
|
3
|
-
type MinimalBncrSendInput = {
|
|
4
|
-
channel?: string;
|
|
5
|
-
action?: string;
|
|
6
|
-
idempotencyKey?: string;
|
|
7
|
-
accountId?: string;
|
|
8
|
-
to?: string;
|
|
9
|
-
message?: string;
|
|
10
|
-
caption?: string;
|
|
11
|
-
path?: string;
|
|
12
|
-
media?: string;
|
|
13
|
-
filePath?: string;
|
|
14
|
-
mediaUrl?: string;
|
|
15
|
-
mediaUrls?: string[];
|
|
16
|
-
asVoice?: boolean;
|
|
17
|
-
audioAsVoice?: boolean;
|
|
18
|
-
params?: Record<string, unknown>;
|
|
19
|
-
};
|
|
20
|
-
|
|
21
|
-
type BuiltBncrMessageAction = {
|
|
22
|
-
channel: string;
|
|
23
|
-
action: string;
|
|
24
|
-
idempotencyKey: string;
|
|
25
|
-
accountId?: string;
|
|
26
|
-
params: Record<string, unknown>;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function asString(v: unknown, fallback = ''): string {
|
|
30
|
-
if (typeof v === 'string') return v;
|
|
31
|
-
if (v == null) return fallback;
|
|
32
|
-
return String(v);
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
36
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function pickFirstString(...values: unknown[]): string | undefined {
|
|
40
|
-
for (const value of values) {
|
|
41
|
-
if (typeof value !== 'string') continue;
|
|
42
|
-
if (value.length === 0) return '';
|
|
43
|
-
return value;
|
|
44
|
-
}
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function pickFirstBoolean(...values: unknown[]): boolean | undefined {
|
|
49
|
-
for (const value of values) {
|
|
50
|
-
if (typeof value === 'boolean') return value;
|
|
51
|
-
}
|
|
52
|
-
return undefined;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export function buildBncrMessageAction(input: MinimalBncrSendInput): BuiltBncrMessageAction {
|
|
56
|
-
const paramsObj = isPlainObject(input.params) ? input.params : {};
|
|
57
|
-
|
|
58
|
-
const channel = asString(input.channel || 'bncr').trim() || 'bncr';
|
|
59
|
-
const action = asString(input.action || 'send').trim() || 'send';
|
|
60
|
-
const idempotencyKey = asString(input.idempotencyKey || '').trim() || `bncr-${randomUUID()}`;
|
|
61
|
-
const accountId =
|
|
62
|
-
asString(pickFirstString(paramsObj.accountId, input.accountId) || '').trim() || undefined;
|
|
63
|
-
|
|
64
|
-
const to = asString(pickFirstString(paramsObj.to, input.to) || '').trim();
|
|
65
|
-
if (!to) throw new Error('bncr send requires to');
|
|
66
|
-
|
|
67
|
-
const mediaPath = pickFirstString(
|
|
68
|
-
paramsObj.media,
|
|
69
|
-
paramsObj.path,
|
|
70
|
-
paramsObj.filePath,
|
|
71
|
-
paramsObj.mediaUrl,
|
|
72
|
-
input.media,
|
|
73
|
-
input.path,
|
|
74
|
-
input.filePath,
|
|
75
|
-
input.mediaUrl,
|
|
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);
|
|
83
|
-
|
|
84
|
-
const message = pickFirstString(paramsObj.message, input.message) ?? '';
|
|
85
|
-
const explicitCaption = pickFirstString(paramsObj.caption, input.caption) ?? '';
|
|
86
|
-
const asVoice = pickFirstBoolean(paramsObj.asVoice, input.asVoice);
|
|
87
|
-
const audioAsVoice = pickFirstBoolean(paramsObj.audioAsVoice, input.audioAsVoice);
|
|
88
|
-
|
|
89
|
-
if ((asVoice === true || audioAsVoice === true) && !mediaPath && !mediaUrls?.length) {
|
|
90
|
-
throw new Error('bncr voice send requires media path');
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const normalizedParams: Record<string, unknown> = {
|
|
94
|
-
...paramsObj,
|
|
95
|
-
to,
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
if (mediaPath) {
|
|
99
|
-
normalizedParams.path = mediaPath;
|
|
100
|
-
const finalCaption = explicitCaption || message;
|
|
101
|
-
if (finalCaption) normalizedParams.caption = finalCaption;
|
|
102
|
-
delete normalizedParams.message;
|
|
103
|
-
if (mediaUrls?.length) normalizedParams.mediaUrls = mediaUrls;
|
|
104
|
-
} else {
|
|
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
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (asVoice === true) normalizedParams.asVoice = true;
|
|
120
|
-
if (audioAsVoice === true) normalizedParams.audioAsVoice = true;
|
|
121
|
-
if (accountId) normalizedParams.accountId = accountId;
|
|
122
|
-
|
|
123
|
-
return {
|
|
124
|
-
channel,
|
|
125
|
-
action,
|
|
126
|
-
idempotencyKey,
|
|
127
|
-
...(accountId ? { accountId } : {}),
|
|
128
|
-
params: normalizedParams,
|
|
129
|
-
};
|
|
130
|
-
}
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ChannelMessageAdapterShape,
|
|
3
|
-
ChannelMessageSendMediaContext,
|
|
4
|
-
ChannelMessageSendPayloadContext,
|
|
5
|
-
ChannelMessageSendResult,
|
|
6
|
-
ChannelMessageSendTextContext,
|
|
7
|
-
} from 'openclaw/plugin-sdk/channel-message';
|
|
8
|
-
import { defineChannelMessageAdapter } from 'openclaw/plugin-sdk/channel-message';
|
|
9
|
-
import type { OpenClawConfig } from 'openclaw/plugin-sdk/core';
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
buildFileTransferOutboxEntry,
|
|
13
|
-
buildTextOutboxEntry,
|
|
14
|
-
} from '../../core/outbox-entry-builders.ts';
|
|
15
|
-
import type { BncrRoute, OutboxEntry } from '../../core/types.ts';
|
|
16
|
-
import { buildBncrDurableQueuedResult } from './durable-queue-adapter.ts';
|
|
17
|
-
|
|
18
|
-
export type BncrDurableMessageQueuedAdapterDeps = {
|
|
19
|
-
enqueueText: (
|
|
20
|
-
ctx: ChannelMessageSendTextContext<OpenClawConfig>,
|
|
21
|
-
) => Promise<OutboxEntry> | OutboxEntry;
|
|
22
|
-
enqueueMedia?: (
|
|
23
|
-
ctx: ChannelMessageSendMediaContext<OpenClawConfig>,
|
|
24
|
-
) => Promise<OutboxEntry> | OutboxEntry;
|
|
25
|
-
enqueuePayload?: (
|
|
26
|
-
ctx: ChannelMessageSendPayloadContext<OpenClawConfig>,
|
|
27
|
-
) => Promise<OutboxEntry> | OutboxEntry;
|
|
28
|
-
now?: () => number;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type BncrDurableMessageQueuedAdapterBuilderDeps = {
|
|
32
|
-
createMessageId: () => string;
|
|
33
|
-
now: () => number;
|
|
34
|
-
normalizeAccountId: (accountId?: string | null) => string;
|
|
35
|
-
normalizeReplyToId: (value?: string | null) => string;
|
|
36
|
-
resolveTarget: (ctx: ChannelMessageSendTextContext<OpenClawConfig>) => {
|
|
37
|
-
route: BncrRoute;
|
|
38
|
-
sessionKey: string;
|
|
39
|
-
accountId?: string | null;
|
|
40
|
-
};
|
|
41
|
-
filePushEvent?: string;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
// This adapter intentionally models only the OpenClaw -> bncr-plugin handoff.
|
|
45
|
-
// Once a message is accepted into bncr's own outbox, OpenClaw should stop managing it;
|
|
46
|
-
// client/platform ACK, retry, and deadLetter remain owned by the bncr service framework.
|
|
47
|
-
export function createBncrDurableMessageQueuedAdapter(
|
|
48
|
-
deps: BncrDurableMessageQueuedAdapterDeps,
|
|
49
|
-
): ChannelMessageAdapterShape<OpenClawConfig, ChannelMessageSendResult> {
|
|
50
|
-
return defineChannelMessageAdapter({
|
|
51
|
-
id: 'bncr-queued-outbox',
|
|
52
|
-
receive: {
|
|
53
|
-
defaultAckPolicy: 'manual',
|
|
54
|
-
supportedAckPolicies: ['manual'],
|
|
55
|
-
},
|
|
56
|
-
send: {
|
|
57
|
-
text: async (ctx) => toChannelMessageSendResult(await deps.enqueueText(ctx), deps.now),
|
|
58
|
-
media: deps.enqueueMedia
|
|
59
|
-
? async (ctx) => toChannelMessageSendResult(await deps.enqueueMedia?.(ctx), deps.now)
|
|
60
|
-
: undefined,
|
|
61
|
-
payload: deps.enqueuePayload
|
|
62
|
-
? async (ctx) => toChannelMessageSendResult(await deps.enqueuePayload?.(ctx), deps.now)
|
|
63
|
-
: undefined,
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function createBncrDurableMessageQueuedAdapterFromBuilders(
|
|
69
|
-
deps: BncrDurableMessageQueuedAdapterBuilderDeps,
|
|
70
|
-
): ChannelMessageAdapterShape<OpenClawConfig, ChannelMessageSendResult> {
|
|
71
|
-
return createBncrDurableMessageQueuedAdapter({
|
|
72
|
-
now: deps.now,
|
|
73
|
-
enqueueText: (ctx) => {
|
|
74
|
-
const resolved = deps.resolveTarget(ctx);
|
|
75
|
-
return buildTextOutboxEntry({
|
|
76
|
-
createMessageId: deps.createMessageId,
|
|
77
|
-
now: deps.now,
|
|
78
|
-
normalizeAccountId: deps.normalizeAccountId,
|
|
79
|
-
normalizeReplyToId: deps.normalizeReplyToId,
|
|
80
|
-
accountId: resolved.accountId ?? ctx.accountId ?? '',
|
|
81
|
-
sessionKey: resolved.sessionKey,
|
|
82
|
-
route: resolved.route,
|
|
83
|
-
text: ctx.text,
|
|
84
|
-
kind: 'final',
|
|
85
|
-
replyToId: ctx.replyToId ?? undefined,
|
|
86
|
-
});
|
|
87
|
-
},
|
|
88
|
-
enqueueMedia: (ctx) => {
|
|
89
|
-
const resolved = deps.resolveTarget(ctx);
|
|
90
|
-
return buildFileTransferOutboxEntry({
|
|
91
|
-
createMessageId: deps.createMessageId,
|
|
92
|
-
now: deps.now,
|
|
93
|
-
normalizeAccountId: deps.normalizeAccountId,
|
|
94
|
-
pushEvent: deps.filePushEvent ?? 'bncr.file.push',
|
|
95
|
-
accountId: resolved.accountId ?? ctx.accountId ?? '',
|
|
96
|
-
sessionKey: resolved.sessionKey,
|
|
97
|
-
route: resolved.route,
|
|
98
|
-
mediaUrl: ctx.mediaUrl,
|
|
99
|
-
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
100
|
-
text: ctx.text,
|
|
101
|
-
asVoice: ctx.audioAsVoice,
|
|
102
|
-
audioAsVoice: ctx.audioAsVoice,
|
|
103
|
-
kind: 'final',
|
|
104
|
-
replyToId: ctx.replyToId ?? undefined,
|
|
105
|
-
});
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function toChannelMessageSendResult(
|
|
111
|
-
entry: OutboxEntry | undefined,
|
|
112
|
-
now?: () => number,
|
|
113
|
-
): ChannelMessageSendResult {
|
|
114
|
-
if (!entry) throw new Error('bncr durable message adapter did not receive an outbox entry');
|
|
115
|
-
const queued = buildBncrDurableQueuedResult({ entry, sentAt: now?.() });
|
|
116
|
-
const receipt: ChannelMessageSendResult['receipt'] = queued.receipt;
|
|
117
|
-
return {
|
|
118
|
-
receipt,
|
|
119
|
-
messageId: queued.receipt.primaryPlatformMessageId,
|
|
120
|
-
};
|
|
121
|
-
}
|
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { normalizeAccountId } from '../../core/accounts.ts';
|
|
2
|
-
import { readOpenClawBooleanParam, readOpenClawStringParam } from '../../openclaw/sdk-helpers.ts';
|
|
3
|
-
import { extractConsumptionFields, parseBncrMarker } from './marker-parser.ts';
|
|
4
|
-
|
|
5
|
-
export type NormalizedBncrSendParams = {
|
|
6
|
-
to: string;
|
|
7
|
-
accountId: string;
|
|
8
|
-
message: string;
|
|
9
|
-
caption: string;
|
|
10
|
-
mediaUrl?: string;
|
|
11
|
-
mediaUrls?: string[];
|
|
12
|
-
asVoice: boolean;
|
|
13
|
-
audioAsVoice: boolean;
|
|
14
|
-
type?: string;
|
|
15
|
-
downloadMedia?: boolean;
|
|
16
|
-
extra?: Record<string, unknown>;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
20
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export function normalizeBncrSendParams(input: {
|
|
24
|
-
params: unknown;
|
|
25
|
-
accountId: string;
|
|
26
|
-
}): NormalizedBncrSendParams {
|
|
27
|
-
const paramsObj = isPlainObject(input.params) ? input.params : {};
|
|
28
|
-
const to = readOpenClawStringParam(paramsObj, 'to', { required: true });
|
|
29
|
-
const resolvedAccountId = normalizeAccountId(
|
|
30
|
-
readOpenClawStringParam(paramsObj, 'accountId') ?? input.accountId,
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const rawMessage = readOpenClawStringParam(paramsObj, 'message', { allowEmpty: true }) ?? '';
|
|
34
|
-
const rawCaption = readOpenClawStringParam(paramsObj, 'caption', { allowEmpty: true }) ?? '';
|
|
35
|
-
// Parse [BncrParam:...] markers from message and caption text
|
|
36
|
-
const { cleanText: parsedMessage, params: msgMarker } = parseBncrMarker(rawMessage);
|
|
37
|
-
const { cleanText: parsedCaption, params: capMarker } = parseBncrMarker(rawCaption);
|
|
38
|
-
const message = parsedMessage;
|
|
39
|
-
const caption = parsedCaption;
|
|
40
|
-
const mediaUrl =
|
|
41
|
-
readOpenClawStringParam(paramsObj, 'media', { trim: false }) ??
|
|
42
|
-
readOpenClawStringParam(paramsObj, 'path', { trim: false }) ??
|
|
43
|
-
readOpenClawStringParam(paramsObj, 'filePath', { trim: false }) ??
|
|
44
|
-
readOpenClawStringParam(paramsObj, 'mediaUrl', { trim: false });
|
|
45
|
-
const rawMediaUrls = paramsObj.mediaUrls;
|
|
46
|
-
const mediaUrls = Array.isArray(rawMediaUrls)
|
|
47
|
-
? Array.from(
|
|
48
|
-
new Set(rawMediaUrls.map((v) => (typeof v === 'string' ? v.trim() : '')).filter(Boolean)),
|
|
49
|
-
)
|
|
50
|
-
: undefined;
|
|
51
|
-
// 如果 mediaUrl 已经在 mediaUrls 中,去重避免重复发送
|
|
52
|
-
const dedupedMediaUrls = mediaUrls?.length
|
|
53
|
-
? mediaUrl && mediaUrls.includes(mediaUrl)
|
|
54
|
-
? mediaUrls
|
|
55
|
-
: mediaUrl
|
|
56
|
-
? [mediaUrl, ...mediaUrls]
|
|
57
|
-
: mediaUrls
|
|
58
|
-
: undefined;
|
|
59
|
-
const asVoice = readOpenClawBooleanParam(paramsObj, 'asVoice') ?? false;
|
|
60
|
-
const audioAsVoice = readOpenClawBooleanParam(paramsObj, 'audioAsVoice') ?? false;
|
|
61
|
-
const type = readOpenClawStringParam(paramsObj, 'type') || undefined;
|
|
62
|
-
const rawExtra = paramsObj.extra;
|
|
63
|
-
const paramsExtra = isPlainObject(rawExtra) ? { ...rawExtra } : {};
|
|
64
|
-
// Merge marker params into extra (marker takes priority over direct extra)
|
|
65
|
-
const mergedExtra = { ...paramsExtra, ...msgMarker, ...capMarker };
|
|
66
|
-
const extra = Object.keys(mergedExtra).length > 0 ? mergedExtra : undefined;
|
|
67
|
-
|
|
68
|
-
// Extract consumption fields from merged extra - marker-specified values override params
|
|
69
|
-
const { consumed, remaining } = extractConsumptionFields(
|
|
70
|
-
Object.keys(mergedExtra).length > 0 ? mergedExtra : undefined,
|
|
71
|
-
);
|
|
72
|
-
const effectiveAsVoice = consumed.asVoice ?? asVoice;
|
|
73
|
-
const effectiveAudioAsVoice = consumed.audioAsVoice ?? audioAsVoice;
|
|
74
|
-
const effectiveDownloadMedia = consumed.downloadMedia === true;
|
|
75
|
-
const effectiveType = consumed.type ?? type;
|
|
76
|
-
const cleanedExtra = Object.keys(remaining).length > 0 ? remaining : undefined;
|
|
77
|
-
|
|
78
|
-
const hasMedia = Boolean(mediaUrl || dedupedMediaUrls?.length);
|
|
79
|
-
|
|
80
|
-
if (effectiveAsVoice && !hasMedia) throw new Error('send voice requires media path');
|
|
81
|
-
|
|
82
|
-
const normalizedMessage = hasMedia ? '' : message || caption || '';
|
|
83
|
-
const normalizedCaption = hasMedia ? caption || message || '' : '';
|
|
84
|
-
|
|
85
|
-
if (!normalizedMessage.trim() && !normalizedCaption.trim() && !hasMedia && !extra) {
|
|
86
|
-
throw new Error('send requires message, media, or extra params');
|
|
87
|
-
}
|
|
88
|
-
console.log(
|
|
89
|
-
'[bncr] normalizeBncrSendParams rawMessage=' +
|
|
90
|
-
JSON.stringify(rawMessage) +
|
|
91
|
-
'|msgMarker=' +
|
|
92
|
-
JSON.stringify(msgMarker) +
|
|
93
|
-
'|consumed=' +
|
|
94
|
-
JSON.stringify(consumed) +
|
|
95
|
-
'|effectiveDownloadMedia=' +
|
|
96
|
-
effectiveDownloadMedia +
|
|
97
|
-
'|mediaUrl=' +
|
|
98
|
-
JSON.stringify(mediaUrl),
|
|
99
|
-
);
|
|
100
|
-
|
|
101
|
-
return {
|
|
102
|
-
to,
|
|
103
|
-
accountId: resolvedAccountId,
|
|
104
|
-
message: normalizedMessage,
|
|
105
|
-
caption: normalizedCaption,
|
|
106
|
-
mediaUrl: dedupedMediaUrls?.length ? undefined : mediaUrl || undefined,
|
|
107
|
-
mediaUrls: dedupedMediaUrls,
|
|
108
|
-
asVoice: effectiveAsVoice,
|
|
109
|
-
...(effectiveDownloadMedia ? { downloadMedia: true } : {}),
|
|
110
|
-
audioAsVoice: effectiveAudioAsVoice,
|
|
111
|
-
...(effectiveType ? { type: effectiveType } : {}),
|
|
112
|
-
...(cleanedExtra ? { extra: cleanedExtra } : {}),
|
|
113
|
-
};
|
|
114
|
-
}
|