@xmoxmo/bncr 0.2.7 → 0.2.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/scripts/check-pack.mjs +21 -5
- package/src/channel.ts +286 -614
- package/src/core/extended-diagnostics.ts +10 -0
- package/src/core/file-ack.ts +9 -0
- package/src/core/file-transfer-payloads.ts +72 -0
- package/src/core/register-trace.ts +79 -0
- package/src/messaging/inbound/runtime-compat.ts +3 -1
- package/src/plugin/capabilities.ts +8 -0
- package/src/plugin/config.ts +35 -0
- package/src/plugin/gateway-methods.ts +12 -0
- package/src/plugin/gateway-runtime.ts +11 -0
- package/src/plugin/message-policy.ts +4 -0
- package/src/plugin/message-send.ts +13 -0
- package/src/plugin/messaging.ts +142 -0
- package/src/plugin/meta.ts +10 -0
- package/src/plugin/outbound.ts +51 -0
- package/src/plugin/setup.ts +24 -0
- package/src/plugin/status.ts +38 -0
- package/src/runtime/log-dedupe.ts +56 -0
- package/src/runtime/outbound-ack-timeout.ts +96 -0
- package/src/runtime/outbound-flags.ts +81 -0
- package/src/runtime/outbox-transitions.ts +119 -0
- package/src/runtime/status-snapshots.ts +108 -0
- package/src/runtime/status-worker.ts +172 -0
|
@@ -2,6 +2,10 @@ import type { RegisterTraceEntry } from './register-trace.ts';
|
|
|
2
2
|
|
|
3
3
|
type ExtendedDiagnosticsInput = {
|
|
4
4
|
diagnostics: Record<string, any>;
|
|
5
|
+
runtimeSurface?: {
|
|
6
|
+
channel: Record<string, boolean>;
|
|
7
|
+
missing: string[];
|
|
8
|
+
};
|
|
5
9
|
register: {
|
|
6
10
|
bridgeId: string;
|
|
7
11
|
gatewayPid: number;
|
|
@@ -48,6 +52,12 @@ type ExtendedDiagnosticsInput = {
|
|
|
48
52
|
export function buildExtendedDiagnostics(input: ExtendedDiagnosticsInput) {
|
|
49
53
|
return {
|
|
50
54
|
...input.diagnostics,
|
|
55
|
+
runtimeSurface: input.runtimeSurface
|
|
56
|
+
? {
|
|
57
|
+
channel: { ...input.runtimeSurface.channel },
|
|
58
|
+
missing: input.runtimeSurface.missing.slice(),
|
|
59
|
+
}
|
|
60
|
+
: undefined,
|
|
51
61
|
register: {
|
|
52
62
|
...input.register,
|
|
53
63
|
traceRecent: input.register.traceRecent.slice(),
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export function buildFileAckKey(args: {
|
|
2
|
+
transferId: string;
|
|
3
|
+
stage: string;
|
|
4
|
+
chunkIndex?: number;
|
|
5
|
+
}): string {
|
|
6
|
+
const n = Number(args.chunkIndex);
|
|
7
|
+
const idx = Number.isInteger(n) && n >= 0 ? String(n) : '-';
|
|
8
|
+
return `${args.transferId}|${args.stage}|${idx}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { BncrRoute } from './accounts.ts';
|
|
2
|
+
|
|
3
|
+
export function buildFileTransferInitPayload(args: {
|
|
4
|
+
transferId: string;
|
|
5
|
+
sessionKey: string;
|
|
6
|
+
route: BncrRoute;
|
|
7
|
+
fileName: string;
|
|
8
|
+
mimeType?: string;
|
|
9
|
+
fileSize: number;
|
|
10
|
+
chunkSize: number;
|
|
11
|
+
totalChunks: number;
|
|
12
|
+
fileSha256: string;
|
|
13
|
+
ts: number;
|
|
14
|
+
}) {
|
|
15
|
+
return {
|
|
16
|
+
transferId: args.transferId,
|
|
17
|
+
direction: 'oc2bncr' as const,
|
|
18
|
+
sessionKey: args.sessionKey,
|
|
19
|
+
platform: args.route.platform,
|
|
20
|
+
groupId: args.route.groupId,
|
|
21
|
+
userId: args.route.userId,
|
|
22
|
+
fileName: args.fileName,
|
|
23
|
+
mimeType: args.mimeType,
|
|
24
|
+
fileSize: args.fileSize,
|
|
25
|
+
chunkSize: args.chunkSize,
|
|
26
|
+
totalChunks: args.totalChunks,
|
|
27
|
+
fileSha256: args.fileSha256,
|
|
28
|
+
ts: args.ts,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildFileTransferChunkPayload(args: {
|
|
33
|
+
transferId: string;
|
|
34
|
+
chunkIndex: number;
|
|
35
|
+
offset: number;
|
|
36
|
+
size: number;
|
|
37
|
+
chunkSha256: string;
|
|
38
|
+
base64: string;
|
|
39
|
+
ts: number;
|
|
40
|
+
}) {
|
|
41
|
+
return {
|
|
42
|
+
transferId: args.transferId,
|
|
43
|
+
chunkIndex: args.chunkIndex,
|
|
44
|
+
offset: args.offset,
|
|
45
|
+
size: args.size,
|
|
46
|
+
chunkSha256: args.chunkSha256,
|
|
47
|
+
base64: args.base64,
|
|
48
|
+
ts: args.ts,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildFileTransferAbortPayload(args: {
|
|
53
|
+
transferId: string;
|
|
54
|
+
reason: string;
|
|
55
|
+
ts: number;
|
|
56
|
+
}) {
|
|
57
|
+
return {
|
|
58
|
+
transferId: args.transferId,
|
|
59
|
+
reason: args.reason,
|
|
60
|
+
ts: args.ts,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function buildFileTransferCompletePayload(args: {
|
|
65
|
+
transferId: string;
|
|
66
|
+
ts: number;
|
|
67
|
+
}) {
|
|
68
|
+
return {
|
|
69
|
+
transferId: args.transferId,
|
|
70
|
+
ts: args.ts,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
@@ -33,6 +33,19 @@ export type RegisterTraceSummary = {
|
|
|
33
33
|
likelyStartupFanoutOnly: boolean;
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
+
export type RegisterDriftSnapshot = {
|
|
37
|
+
capturedAt: number;
|
|
38
|
+
registerCount: number;
|
|
39
|
+
apiGeneration: number;
|
|
40
|
+
postWarmupRegisterCount: number;
|
|
41
|
+
apiInstanceId: string | null;
|
|
42
|
+
registryFingerprint: string | null;
|
|
43
|
+
dominantBucket: string | null;
|
|
44
|
+
sourceBuckets: Record<string, number>;
|
|
45
|
+
traceWindowSize: number;
|
|
46
|
+
traceRecent: Array<Record<string, unknown>>;
|
|
47
|
+
};
|
|
48
|
+
|
|
36
49
|
export function classifyRegisterTrace(stack: string) {
|
|
37
50
|
if (
|
|
38
51
|
stack.includes('prepareSecretsRuntimeSnapshot') ||
|
|
@@ -68,6 +81,72 @@ export function dominantRegisterBucket(sourceBuckets: Record<string, number>) {
|
|
|
68
81
|
return winner;
|
|
69
82
|
}
|
|
70
83
|
|
|
84
|
+
export function buildRegisterTraceEntry(args: {
|
|
85
|
+
ts: number;
|
|
86
|
+
bridgeId: string;
|
|
87
|
+
gatewayPid: number;
|
|
88
|
+
registerCount: number;
|
|
89
|
+
apiGeneration: number;
|
|
90
|
+
apiRebound: boolean;
|
|
91
|
+
apiInstanceId: string | null;
|
|
92
|
+
registryFingerprint: string | null;
|
|
93
|
+
source: string | null;
|
|
94
|
+
pluginVersion: string | null;
|
|
95
|
+
stack: string;
|
|
96
|
+
}): RegisterTraceEntry {
|
|
97
|
+
return {
|
|
98
|
+
ts: args.ts,
|
|
99
|
+
bridgeId: args.bridgeId,
|
|
100
|
+
gatewayPid: args.gatewayPid,
|
|
101
|
+
registerCount: args.registerCount,
|
|
102
|
+
apiGeneration: args.apiGeneration,
|
|
103
|
+
apiRebound: args.apiRebound,
|
|
104
|
+
apiInstanceId: args.apiInstanceId,
|
|
105
|
+
registryFingerprint: args.registryFingerprint,
|
|
106
|
+
source: args.source,
|
|
107
|
+
pluginVersion: args.pluginVersion,
|
|
108
|
+
stack: args.stack,
|
|
109
|
+
stackBucket: classifyRegisterTrace(args.stack),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function appendBoundedRegisterTrace(
|
|
114
|
+
traceRecent: RegisterTraceEntry[],
|
|
115
|
+
trace: RegisterTraceEntry,
|
|
116
|
+
maxEntries = 12,
|
|
117
|
+
) {
|
|
118
|
+
traceRecent.push(trace);
|
|
119
|
+
const cap = Math.max(0, Math.floor(finiteNumberOr(maxEntries, 12)));
|
|
120
|
+
if (cap === 0) {
|
|
121
|
+
traceRecent.splice(0, traceRecent.length);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (traceRecent.length > cap) traceRecent.splice(0, traceRecent.length - cap);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function buildRegisterDriftSnapshot(args: {
|
|
128
|
+
capturedAt: number;
|
|
129
|
+
registerCount: number;
|
|
130
|
+
apiGeneration: number;
|
|
131
|
+
summary: RegisterTraceSummary;
|
|
132
|
+
apiInstanceId: string | null;
|
|
133
|
+
registryFingerprint: string | null;
|
|
134
|
+
traceRecent: RegisterTraceEntry[];
|
|
135
|
+
}): RegisterDriftSnapshot {
|
|
136
|
+
return {
|
|
137
|
+
capturedAt: args.capturedAt,
|
|
138
|
+
registerCount: args.registerCount,
|
|
139
|
+
apiGeneration: args.apiGeneration,
|
|
140
|
+
postWarmupRegisterCount: args.summary.postWarmupRegisterCount,
|
|
141
|
+
apiInstanceId: args.apiInstanceId,
|
|
142
|
+
registryFingerprint: args.registryFingerprint,
|
|
143
|
+
dominantBucket: args.summary.dominantBucket,
|
|
144
|
+
sourceBuckets: { ...args.summary.sourceBuckets },
|
|
145
|
+
traceWindowSize: args.traceRecent.length,
|
|
146
|
+
traceRecent: args.traceRecent.map((trace) => ({ ...trace })),
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
71
150
|
export function buildRegisterTraceSummary(args: {
|
|
72
151
|
traceRecent: RegisterTraceEntry[];
|
|
73
152
|
firstRegisterAt: number | null;
|
|
@@ -20,9 +20,11 @@ export function resolveBncrChannelInboundRuntime(api: any): ChannelRuntimeCompat
|
|
|
20
20
|
if (legacyTurnRuntime?.buildContext && legacyTurnRuntime?.run) {
|
|
21
21
|
if (!warnedLegacyTurnRuntime) {
|
|
22
22
|
warnedLegacyTurnRuntime = true;
|
|
23
|
+
const channelRuntimeKeys = Object.keys(channelRuntime ?? {}).sort().join(',') || 'none';
|
|
24
|
+
const inboundRuntimeKeys = Object.keys(inboundRuntime ?? {}).sort().join(',') || 'none';
|
|
23
25
|
emitBncrLogLine(
|
|
24
26
|
'warn',
|
|
25
|
-
|
|
27
|
+
`[bncr] inbound runtime fallback=turn|preferred=inbound|channelKeys=${channelRuntimeKeys}|inboundKeys=${inboundRuntimeKeys}`,
|
|
26
28
|
);
|
|
27
29
|
}
|
|
28
30
|
return {
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CHANNEL_ID,
|
|
3
|
+
listAccountIds,
|
|
4
|
+
resolveAccount,
|
|
5
|
+
resolveDefaultDisplayName,
|
|
6
|
+
} from '../core/accounts.ts';
|
|
7
|
+
import { resolveBncrChannelPolicy } from '../core/policy.ts';
|
|
8
|
+
import { setOpenClawAccountEnabledInConfigSection } from '../openclaw/sdk-helpers.ts';
|
|
9
|
+
|
|
10
|
+
export const BNCR_CONFIG_SURFACE = {
|
|
11
|
+
listAccountIds,
|
|
12
|
+
resolveAccount,
|
|
13
|
+
setAccountEnabled: ({ cfg, accountId, enabled }: any) =>
|
|
14
|
+
setOpenClawAccountEnabledInConfigSection({
|
|
15
|
+
cfg,
|
|
16
|
+
sectionKey: CHANNEL_ID,
|
|
17
|
+
accountId,
|
|
18
|
+
enabled,
|
|
19
|
+
allowTopLevel: true,
|
|
20
|
+
}),
|
|
21
|
+
isEnabled: (account: any, cfg: any) => {
|
|
22
|
+
const policy = resolveBncrChannelPolicy(cfg?.channels?.[CHANNEL_ID] || {});
|
|
23
|
+
return policy.enabled !== false && account?.enabled !== false;
|
|
24
|
+
},
|
|
25
|
+
isConfigured: () => true,
|
|
26
|
+
describeAccount: (account: any) => {
|
|
27
|
+
const displayName = resolveDefaultDisplayName(account?.name, account?.accountId);
|
|
28
|
+
return {
|
|
29
|
+
accountId: account.accountId,
|
|
30
|
+
name: displayName,
|
|
31
|
+
enabled: account.enabled !== false,
|
|
32
|
+
configured: true,
|
|
33
|
+
};
|
|
34
|
+
},
|
|
35
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type BncrGatewayAccountBridge = {
|
|
2
|
+
channelStartAccount: (ctx: any) => unknown | Promise<unknown>;
|
|
3
|
+
channelStopAccount: (ctx: any) => unknown | Promise<unknown>;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export function createBncrGatewayRuntime(getBridge: () => BncrGatewayAccountBridge) {
|
|
7
|
+
return {
|
|
8
|
+
startAccount: async (ctx: any) => getBridge().channelStartAccount(ctx),
|
|
9
|
+
stopAccount: async (ctx: any) => getBridge().channelStopAccount(ctx),
|
|
10
|
+
};
|
|
11
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export type BncrMessageSendBridge = {
|
|
2
|
+
channelMessageSendText: (ctx: any) => unknown | Promise<unknown>;
|
|
3
|
+
channelMessageSendMedia: (ctx: any) => unknown | Promise<unknown>;
|
|
4
|
+
channelMessageSendPayload: (ctx: any) => unknown | Promise<unknown>;
|
|
5
|
+
};
|
|
6
|
+
|
|
7
|
+
export function createBncrMessageSend(getBridge: () => BncrMessageSendBridge) {
|
|
8
|
+
return {
|
|
9
|
+
text: async (ctx: any) => getBridge().channelMessageSendText(ctx),
|
|
10
|
+
media: async (ctx: any) => getBridge().channelMessageSendMedia(ctx),
|
|
11
|
+
payload: async (ctx: any) => getBridge().channelMessageSendPayload(ctx),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import { BNCR_DEFAULT_ACCOUNT_ID, normalizeAccountId } from '../core/accounts.ts';
|
|
2
|
+
import {
|
|
3
|
+
formatDisplayScope,
|
|
4
|
+
formatTargetDisplay,
|
|
5
|
+
parseExplicitTarget,
|
|
6
|
+
} from '../core/targets.ts';
|
|
7
|
+
import { resolveBncrOutboundSessionRoute } from '../messaging/outbound/session-route.ts';
|
|
8
|
+
import {
|
|
9
|
+
looksLikeBncrExplicitTarget,
|
|
10
|
+
resolveBncrOutboundTarget,
|
|
11
|
+
} from '../messaging/outbound/target-resolver.ts';
|
|
12
|
+
|
|
13
|
+
type BncrMessagingRuntimeBridge = {
|
|
14
|
+
canonicalAgentId?: string;
|
|
15
|
+
ensureCanonicalAgentId: (params: { cfg: any; accountId: string }) => string;
|
|
16
|
+
resolveRouteBySession: (raw: string, accountId: string) => any;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
function asString(v: unknown, fallback = ''): string {
|
|
20
|
+
return typeof v === 'string' ? v : fallback;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function normalizeBncrMessagingTarget(raw: string) {
|
|
24
|
+
const input = asString(raw).trim();
|
|
25
|
+
return input || undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function formatBncrMessagingTargetDisplay({ target }: any) {
|
|
29
|
+
return formatTargetDisplay(target);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveMessagingAccountId(accountId: unknown) {
|
|
33
|
+
return normalizeAccountId(asString(accountId || BNCR_DEFAULT_ACCOUNT_ID));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function resolveMessagingCanonicalAgentId(
|
|
37
|
+
runtimeBridge: BncrMessagingRuntimeBridge,
|
|
38
|
+
cfg: any,
|
|
39
|
+
accountId: string,
|
|
40
|
+
) {
|
|
41
|
+
return (
|
|
42
|
+
runtimeBridge.canonicalAgentId ||
|
|
43
|
+
runtimeBridge.ensureCanonicalAgentId({ cfg, accountId })
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function createBncrMessagingExplicitTargetParser(
|
|
48
|
+
getBridge: () => BncrMessagingRuntimeBridge,
|
|
49
|
+
) {
|
|
50
|
+
return ({ raw, accountId, cfg }: any) => {
|
|
51
|
+
const resolvedAccountId = resolveMessagingAccountId(accountId);
|
|
52
|
+
const runtimeBridge = getBridge();
|
|
53
|
+
const canonicalAgentId = resolveMessagingCanonicalAgentId(
|
|
54
|
+
runtimeBridge,
|
|
55
|
+
cfg,
|
|
56
|
+
resolvedAccountId,
|
|
57
|
+
);
|
|
58
|
+
return parseExplicitTarget(asString(raw).trim(), { canonicalAgentId });
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function createBncrMessagingSessionTargetResolver(
|
|
63
|
+
getBridge: () => BncrMessagingRuntimeBridge,
|
|
64
|
+
) {
|
|
65
|
+
return ({ id, accountId, cfg }: any) => {
|
|
66
|
+
const raw = asString(id).trim();
|
|
67
|
+
if (!raw) return undefined;
|
|
68
|
+
const resolvedAccountId = resolveMessagingAccountId(accountId);
|
|
69
|
+
const runtimeBridge = getBridge();
|
|
70
|
+
const canonicalAgentId = resolveMessagingCanonicalAgentId(
|
|
71
|
+
runtimeBridge,
|
|
72
|
+
cfg,
|
|
73
|
+
resolvedAccountId,
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
let parsed = parseExplicitTarget(raw, { canonicalAgentId });
|
|
77
|
+
if (!parsed) {
|
|
78
|
+
const route = runtimeBridge.resolveRouteBySession(raw, resolvedAccountId);
|
|
79
|
+
if (route) {
|
|
80
|
+
parsed = parseExplicitTarget(formatDisplayScope(route), { canonicalAgentId });
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return parsed?.displayScope || undefined;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function createBncrMessagingOutboundSessionRouteResolver(
|
|
88
|
+
getBridge: () => BncrMessagingRuntimeBridge,
|
|
89
|
+
) {
|
|
90
|
+
return (params: any) => {
|
|
91
|
+
const accountId = resolveMessagingAccountId(params?.accountId);
|
|
92
|
+
const runtimeBridge = getBridge();
|
|
93
|
+
const canonicalAgentId = resolveMessagingCanonicalAgentId(
|
|
94
|
+
runtimeBridge,
|
|
95
|
+
params?.cfg,
|
|
96
|
+
accountId,
|
|
97
|
+
);
|
|
98
|
+
return resolveBncrOutboundSessionRoute({
|
|
99
|
+
...params,
|
|
100
|
+
canonicalAgentId,
|
|
101
|
+
resolveRouteBySession: (raw: string, acc: string) =>
|
|
102
|
+
runtimeBridge.resolveRouteBySession(raw, acc),
|
|
103
|
+
});
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export function createBncrMessagingSurface(getBridge: () => BncrMessagingRuntimeBridge) {
|
|
108
|
+
return {
|
|
109
|
+
// 接收任意标签输入;不在 normalize 阶段做格式门槛,统一下沉到发送前验证。
|
|
110
|
+
normalizeTarget: normalizeBncrMessagingTarget,
|
|
111
|
+
parseExplicitTarget: createBncrMessagingExplicitTargetParser(getBridge),
|
|
112
|
+
formatTargetDisplay: formatBncrMessagingTargetDisplay,
|
|
113
|
+
resolveSessionTarget: createBncrMessagingSessionTargetResolver(getBridge),
|
|
114
|
+
resolveOutboundSessionRoute: createBncrMessagingOutboundSessionRouteResolver(getBridge),
|
|
115
|
+
targetResolver: createBncrMessagingTargetResolver(getBridge),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function createBncrMessagingTargetResolver(getBridge: () => BncrMessagingRuntimeBridge) {
|
|
120
|
+
return {
|
|
121
|
+
looksLikeId: (raw: string, normalized?: string) => {
|
|
122
|
+
return looksLikeBncrExplicitTarget(asString(normalized || raw).trim());
|
|
123
|
+
},
|
|
124
|
+
resolveTarget: async ({ accountId, input, normalized }: any) => {
|
|
125
|
+
const runtimeBridge = getBridge();
|
|
126
|
+
const resolved = resolveBncrOutboundTarget({
|
|
127
|
+
target: asString(normalized || input).trim(),
|
|
128
|
+
accountId: resolveMessagingAccountId(accountId),
|
|
129
|
+
resolveRouteBySession: (raw: string, acc: string) =>
|
|
130
|
+
runtimeBridge.resolveRouteBySession(raw, acc),
|
|
131
|
+
});
|
|
132
|
+
if (!resolved) return null;
|
|
133
|
+
return {
|
|
134
|
+
to: resolved.displayScope,
|
|
135
|
+
kind: resolved.kind,
|
|
136
|
+
display: resolved.displayScope,
|
|
137
|
+
source: 'normalized' as const,
|
|
138
|
+
};
|
|
139
|
+
},
|
|
140
|
+
hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent:<agentId>:bncr:direct:<hex>',
|
|
141
|
+
};
|
|
142
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { normalizeAccountId } from '../core/accounts.ts';
|
|
2
|
+
import {
|
|
3
|
+
deleteBncrMessageAction,
|
|
4
|
+
editBncrMessageAction,
|
|
5
|
+
reactBncrMessageAction,
|
|
6
|
+
sendBncrReplyAction,
|
|
7
|
+
} 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
|
+
}
|
|
12
|
+
|
|
13
|
+
export type BncrOutboundBridge = {
|
|
14
|
+
channelSendText: (ctx: any) => unknown | Promise<unknown>;
|
|
15
|
+
channelSendMedia: (ctx: any) => unknown | Promise<unknown>;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export function createBncrOutboundRuntime(getBridge: () => BncrOutboundBridge) {
|
|
19
|
+
return {
|
|
20
|
+
deliveryMode: 'gateway' as const,
|
|
21
|
+
sendText: async (ctx: any) => getBridge().channelSendText(ctx),
|
|
22
|
+
sendMedia: async (ctx: any) => getBridge().channelSendMedia(ctx),
|
|
23
|
+
replyAction: async (ctx: any) =>
|
|
24
|
+
sendBncrReplyAction({
|
|
25
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
26
|
+
to: asString(ctx?.to || '').trim(),
|
|
27
|
+
text: asString(ctx?.text || ''),
|
|
28
|
+
replyToMessageId:
|
|
29
|
+
asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
30
|
+
sendText: async ({ accountId, to, text }) =>
|
|
31
|
+
getBridge().channelSendText({ accountId, to, text }),
|
|
32
|
+
}),
|
|
33
|
+
deleteAction: async (ctx: any) =>
|
|
34
|
+
deleteBncrMessageAction({
|
|
35
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
36
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
37
|
+
}),
|
|
38
|
+
reactAction: async (ctx: any) =>
|
|
39
|
+
reactBncrMessageAction({
|
|
40
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
41
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
42
|
+
emoji: asString(ctx?.emoji || '').trim(),
|
|
43
|
+
}),
|
|
44
|
+
editAction: async (ctx: any) =>
|
|
45
|
+
editBncrMessageAction({
|
|
46
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
47
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
48
|
+
text: asString(ctx?.text || ''),
|
|
49
|
+
}),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { CHANNEL_ID } from '../core/accounts.ts';
|
|
2
|
+
import { applyOpenClawAccountNameToChannelSection } from '../openclaw/sdk-helpers.ts';
|
|
3
|
+
|
|
4
|
+
export const BNCR_SETUP_SURFACE = {
|
|
5
|
+
applyAccountName: ({ cfg, accountId, name }: any) =>
|
|
6
|
+
applyOpenClawAccountNameToChannelSection({
|
|
7
|
+
cfg,
|
|
8
|
+
channelKey: CHANNEL_ID,
|
|
9
|
+
accountId,
|
|
10
|
+
name,
|
|
11
|
+
alwaysUseAccounts: true,
|
|
12
|
+
}),
|
|
13
|
+
applyAccountConfig: ({ cfg, accountId }: any) => {
|
|
14
|
+
const next = { ...(cfg || {}) } as any;
|
|
15
|
+
next.channels = next.channels || {};
|
|
16
|
+
next.channels[CHANNEL_ID] = next.channels[CHANNEL_ID] || {};
|
|
17
|
+
next.channels[CHANNEL_ID].accounts = next.channels[CHANNEL_ID].accounts || {};
|
|
18
|
+
next.channels[CHANNEL_ID].accounts[accountId] = {
|
|
19
|
+
...(next.channels[CHANNEL_ID].accounts[accountId] || {}),
|
|
20
|
+
enabled: true,
|
|
21
|
+
};
|
|
22
|
+
return next;
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { BNCR_DEFAULT_ACCOUNT_ID, resolveAccount, resolveDefaultDisplayName } from '../core/accounts.ts';
|
|
2
|
+
import { buildAccountStatusSnapshot } from '../core/status.ts';
|
|
3
|
+
import { createOpenClawDefaultChannelRuntimeState } from '../openclaw/sdk-helpers.ts';
|
|
4
|
+
|
|
5
|
+
export type BncrStatusBridge = {
|
|
6
|
+
getChannelSummary: (defaultAccountId: string) => unknown | Promise<unknown>;
|
|
7
|
+
getAccountRuntimeSnapshot: (accountId?: string) => unknown;
|
|
8
|
+
getStatusHeadline: (accountId?: string) => unknown;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export function createBncrStatusSurface(getBridge: () => BncrStatusBridge) {
|
|
12
|
+
return {
|
|
13
|
+
defaultRuntime: createOpenClawDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
|
|
14
|
+
mode: 'ws-offline',
|
|
15
|
+
}),
|
|
16
|
+
buildChannelSummary: async ({ defaultAccountId }: any) => {
|
|
17
|
+
return getBridge().getChannelSummary(defaultAccountId || BNCR_DEFAULT_ACCOUNT_ID);
|
|
18
|
+
},
|
|
19
|
+
buildAccountSnapshot: async ({ account, runtime }: any) => {
|
|
20
|
+
const runtimeBridge = getBridge();
|
|
21
|
+
const rt = runtime || runtimeBridge.getAccountRuntimeSnapshot(account?.accountId);
|
|
22
|
+
return buildAccountStatusSnapshot({
|
|
23
|
+
account,
|
|
24
|
+
runtime: rt,
|
|
25
|
+
healthSummary: runtimeBridge.getStatusHeadline(account?.accountId),
|
|
26
|
+
// default 名不可隐藏时,统一展示稳定默认值
|
|
27
|
+
displayName: resolveDefaultDisplayName(account?.name, account?.accountId),
|
|
28
|
+
});
|
|
29
|
+
},
|
|
30
|
+
resolveAccountState: ({ enabled, configured, account, cfg, runtime }: any) => {
|
|
31
|
+
if (!enabled) return 'disabled';
|
|
32
|
+
const resolved = resolveAccount(cfg, account?.accountId);
|
|
33
|
+
if (!(resolved.enabled && configured)) return 'not configured';
|
|
34
|
+
const rt = runtime || getBridge().getAccountRuntimeSnapshot(account?.accountId);
|
|
35
|
+
return (rt as any)?.connected ? 'linked' : 'configured';
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export type LogDedupeStateEntry = {
|
|
2
|
+
at: number;
|
|
3
|
+
sig: string;
|
|
4
|
+
};
|
|
5
|
+
|
|
6
|
+
export type LogDedupeState = Map<string, LogDedupeStateEntry>;
|
|
7
|
+
|
|
8
|
+
export const LOG_DEDUPE_STATE_TTL_MS = 10 * 60 * 1000;
|
|
9
|
+
export const LOG_DEDUPE_STATE_MAX_ENTRIES = 1_000;
|
|
10
|
+
|
|
11
|
+
export function pruneLogDedupeState(
|
|
12
|
+
state: LogDedupeState,
|
|
13
|
+
currentTime: number,
|
|
14
|
+
options?: {
|
|
15
|
+
ttlMs?: number;
|
|
16
|
+
maxEntries?: number;
|
|
17
|
+
},
|
|
18
|
+
) {
|
|
19
|
+
const ttlMs = options?.ttlMs ?? LOG_DEDUPE_STATE_TTL_MS;
|
|
20
|
+
const maxEntries = options?.maxEntries ?? LOG_DEDUPE_STATE_MAX_ENTRIES;
|
|
21
|
+
|
|
22
|
+
for (const [key, entry] of state.entries()) {
|
|
23
|
+
if (currentTime - entry.at > ttlMs) {
|
|
24
|
+
state.delete(key);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
while (state.size > maxEntries) {
|
|
29
|
+
const oldestKey = state.keys().next().value;
|
|
30
|
+
if (!oldestKey) break;
|
|
31
|
+
state.delete(oldestKey);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function shouldEmitDedupLog(args: {
|
|
36
|
+
state: LogDedupeState;
|
|
37
|
+
key: string;
|
|
38
|
+
sig: string;
|
|
39
|
+
nowMs: number;
|
|
40
|
+
windowMs?: number;
|
|
41
|
+
ttlMs?: number;
|
|
42
|
+
maxEntries?: number;
|
|
43
|
+
}) {
|
|
44
|
+
const windowMs = args.windowMs ?? 5 * 60 * 1000;
|
|
45
|
+
const pruneOptions = {
|
|
46
|
+
ttlMs: args.ttlMs,
|
|
47
|
+
maxEntries: args.maxEntries,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
pruneLogDedupeState(args.state, args.nowMs, pruneOptions);
|
|
51
|
+
const prev = args.state.get(args.key) || null;
|
|
52
|
+
if (prev && prev.sig === args.sig && args.nowMs - prev.at < windowMs) return false;
|
|
53
|
+
args.state.set(args.key, { at: args.nowMs, sig: args.sig });
|
|
54
|
+
pruneLogDedupeState(args.state, args.nowMs, pruneOptions);
|
|
55
|
+
return true;
|
|
56
|
+
}
|