@xmoxmo/bncr 0.0.4 → 0.0.5
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/LICENSE +21 -0
- package/README.md +97 -27
- package/index.ts +2 -2
- package/openclaw.plugin.json +33 -2
- package/package.json +7 -2
- package/scripts/selfcheck.mjs +38 -0
- package/src/channel.ts +226 -734
- package/src/core/accounts.ts +50 -0
- package/src/core/config-schema.ts +42 -0
- package/src/core/permissions.ts +29 -0
- package/src/core/policy.ts +27 -0
- package/src/core/probe.ts +45 -0
- package/src/core/status.ts +145 -0
- package/src/core/targets.ts +243 -0
- package/src/core/types.ts +59 -0
- package/src/messaging/inbound/commands.ts +136 -0
- package/src/messaging/inbound/dispatch.ts +178 -0
- package/src/messaging/inbound/gate.ts +66 -0
- package/src/messaging/inbound/parse.ts +97 -0
- package/src/messaging/outbound/actions.ts +42 -0
- package/src/messaging/outbound/media.ts +53 -0
- package/src/messaging/outbound/send.ts +67 -0
package/src/channel.ts
CHANGED
|
@@ -14,9 +14,40 @@ import {
|
|
|
14
14
|
writeJsonFileAtomically,
|
|
15
15
|
readJsonFileWithFallback,
|
|
16
16
|
} from 'openclaw/plugin-sdk';
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
17
|
+
import { CHANNEL_ID, BNCR_DEFAULT_ACCOUNT_ID, normalizeAccountId, resolveDefaultDisplayName, resolveAccount, listAccountIds } from './core/accounts.js';
|
|
18
|
+
import type { BncrRoute, BncrConnection, OutboxEntry } from './core/types.js';
|
|
19
|
+
import {
|
|
20
|
+
parseRouteFromScope,
|
|
21
|
+
parseRouteFromDisplayScope,
|
|
22
|
+
formatDisplayScope,
|
|
23
|
+
isLowerHex,
|
|
24
|
+
routeScopeToHex,
|
|
25
|
+
parseRouteFromHexScope,
|
|
26
|
+
parseRouteLike,
|
|
27
|
+
parseLegacySessionKeyToStrict,
|
|
28
|
+
normalizeStoredSessionKey,
|
|
29
|
+
parseStrictBncrSessionKey,
|
|
30
|
+
normalizeInboundSessionKey,
|
|
31
|
+
withTaskSessionKey,
|
|
32
|
+
buildFallbackSessionKey,
|
|
33
|
+
routeKey,
|
|
34
|
+
} from './core/targets.js';
|
|
35
|
+
import { parseBncrInboundParams } from './messaging/inbound/parse.js';
|
|
36
|
+
import { dispatchBncrInbound } from './messaging/inbound/dispatch.js';
|
|
37
|
+
import { checkBncrMessageGate } from './messaging/inbound/gate.js';
|
|
38
|
+
import { sendBncrText, sendBncrMedia } from './messaging/outbound/send.js';
|
|
39
|
+
import { buildBncrMediaOutboundFrame, resolveBncrOutboundMessageType } from './messaging/outbound/media.js';
|
|
40
|
+
import { sendBncrReplyAction, deleteBncrMessageAction, reactBncrMessageAction, editBncrMessageAction } from './messaging/outbound/actions.js';
|
|
41
|
+
import {
|
|
42
|
+
buildIntegratedDiagnostics as buildIntegratedDiagnosticsFromRuntime,
|
|
43
|
+
buildStatusHeadlineFromRuntime,
|
|
44
|
+
buildStatusMetaFromRuntime,
|
|
45
|
+
buildAccountRuntimeSnapshot,
|
|
46
|
+
} from './core/status.js';
|
|
47
|
+
import { probeBncrAccount } from './core/probe.js';
|
|
48
|
+
import { BncrConfigSchema } from './core/config-schema.js';
|
|
49
|
+
import { resolveBncrChannelPolicy } from './core/policy.js';
|
|
50
|
+
import { buildBncrPermissionSummary } from './core/permissions.js';
|
|
20
51
|
const BRIDGE_VERSION = 2;
|
|
21
52
|
const BNCR_PUSH_EVENT = 'bncr.push';
|
|
22
53
|
const CONNECT_TTL_MS = 120_000;
|
|
@@ -31,53 +62,6 @@ const FILE_TRANSFER_ACK_TTL_MS = 30_000;
|
|
|
31
62
|
const FILE_TRANSFER_KEEP_MS = 6 * 60 * 60 * 1000;
|
|
32
63
|
const BNCR_DEBUG_VERBOSE = true; // 临时调试:打印发送入口完整请求体
|
|
33
64
|
|
|
34
|
-
const BncrConfigSchema = {
|
|
35
|
-
schema: {
|
|
36
|
-
type: 'object',
|
|
37
|
-
additionalProperties: true,
|
|
38
|
-
properties: {
|
|
39
|
-
accounts: {
|
|
40
|
-
type: 'object',
|
|
41
|
-
additionalProperties: {
|
|
42
|
-
type: 'object',
|
|
43
|
-
additionalProperties: true,
|
|
44
|
-
properties: {
|
|
45
|
-
enabled: { type: 'boolean' },
|
|
46
|
-
name: { type: 'string' },
|
|
47
|
-
},
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
};
|
|
53
|
-
|
|
54
|
-
type BncrRoute = {
|
|
55
|
-
platform: string;
|
|
56
|
-
groupId: string;
|
|
57
|
-
userId: string;
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
type BncrConnection = {
|
|
61
|
-
accountId: string;
|
|
62
|
-
connId: string;
|
|
63
|
-
clientId?: string;
|
|
64
|
-
connectedAt: number;
|
|
65
|
-
lastSeenAt: number;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
type OutboxEntry = {
|
|
69
|
-
messageId: string;
|
|
70
|
-
accountId: string;
|
|
71
|
-
sessionKey: string;
|
|
72
|
-
route: BncrRoute;
|
|
73
|
-
payload: Record<string, unknown>;
|
|
74
|
-
createdAt: number;
|
|
75
|
-
retryCount: number;
|
|
76
|
-
nextAttemptAt: number;
|
|
77
|
-
lastAttemptAt?: number;
|
|
78
|
-
lastError?: string;
|
|
79
|
-
};
|
|
80
|
-
|
|
81
65
|
type FileSendTransferState = {
|
|
82
66
|
transferId: string;
|
|
83
67
|
accountId: string;
|
|
@@ -155,328 +139,12 @@ function asString(v: unknown, fallback = ''): string {
|
|
|
155
139
|
return String(v);
|
|
156
140
|
}
|
|
157
141
|
|
|
158
|
-
function normalizeAccountId(accountId?: string | null): string {
|
|
159
|
-
const v = asString(accountId || '').trim();
|
|
160
|
-
if (!v) return BNCR_DEFAULT_ACCOUNT_ID;
|
|
161
|
-
const lower = v.toLowerCase();
|
|
162
|
-
// 历史兼容:default/primary 统一折叠到 Primary,避免状态尾巴反复出现。
|
|
163
|
-
if (lower === 'default' || lower === 'primary') return BNCR_DEFAULT_ACCOUNT_ID;
|
|
164
|
-
return v;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function parseRouteFromScope(scope: string): BncrRoute | null {
|
|
168
|
-
const parts = asString(scope).trim().split(':');
|
|
169
|
-
if (parts.length < 3) return null;
|
|
170
|
-
const [platform, groupId, userId] = parts;
|
|
171
|
-
if (!platform || !groupId || !userId) return null;
|
|
172
|
-
return { platform, groupId, userId };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function parseRouteFromDisplayScope(scope: string): BncrRoute | null {
|
|
176
|
-
const raw = asString(scope).trim();
|
|
177
|
-
if (!raw) return null;
|
|
178
|
-
|
|
179
|
-
// 终版兼容(6 种):
|
|
180
|
-
// 1) bncr:g-<hex(scope)>
|
|
181
|
-
// 2) bncr:<hex(scope)>
|
|
182
|
-
// 3) bncr:<platform>:<groupId>:<userId>
|
|
183
|
-
// 4) bncr:g-<platform>:<groupId>:<userId>
|
|
184
|
-
|
|
185
|
-
// 1) bncr:g-<hex> 或 bncr:g-<scope>
|
|
186
|
-
const gPayload = raw.match(/^bncr:g-(.+)$/i)?.[1];
|
|
187
|
-
if (gPayload) {
|
|
188
|
-
if (isLowerHex(gPayload)) {
|
|
189
|
-
const route = parseRouteFromHexScope(gPayload);
|
|
190
|
-
if (route) return route;
|
|
191
|
-
}
|
|
192
|
-
return parseRouteFromScope(gPayload);
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// 2) / 3) bncr:<hex> or bncr:<scope>
|
|
196
|
-
const bPayload = raw.match(/^bncr:(.+)$/i)?.[1];
|
|
197
|
-
if (bPayload) {
|
|
198
|
-
if (isLowerHex(bPayload)) {
|
|
199
|
-
const route = parseRouteFromHexScope(bPayload);
|
|
200
|
-
if (route) return route;
|
|
201
|
-
}
|
|
202
|
-
return parseRouteFromScope(bPayload);
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
return null;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
function formatDisplayScope(route: BncrRoute): string {
|
|
209
|
-
// 主推荐标签:bncr:<platform>:<groupId>:<userId>
|
|
210
|
-
// 保持原始大小写,不做平台名降级。
|
|
211
|
-
return `bncr:${route.platform}:${route.groupId}:${route.userId}`;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function isLowerHex(input: string): boolean {
|
|
215
|
-
const raw = asString(input).trim();
|
|
216
|
-
// 兼容大小写十六进制,不主动降级大小写
|
|
217
|
-
return !!raw && /^[0-9a-fA-F]+$/.test(raw) && raw.length % 2 === 0;
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
function routeScopeToHex(route: BncrRoute): string {
|
|
221
|
-
const raw = `${route.platform}:${route.groupId}:${route.userId}`;
|
|
222
|
-
return Buffer.from(raw, 'utf8').toString('hex').toLowerCase();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
function parseRouteFromHexScope(scopeHex: string): BncrRoute | null {
|
|
226
|
-
const rawHex = asString(scopeHex).trim();
|
|
227
|
-
if (!isLowerHex(rawHex)) return null;
|
|
228
|
-
|
|
229
|
-
try {
|
|
230
|
-
const decoded = Buffer.from(rawHex, 'hex').toString('utf8');
|
|
231
|
-
return parseRouteFromScope(decoded);
|
|
232
|
-
} catch {
|
|
233
|
-
return null;
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
function parseRouteLike(input: unknown): BncrRoute | null {
|
|
238
|
-
const platform = asString((input as any)?.platform || '').trim();
|
|
239
|
-
const groupId = asString((input as any)?.groupId || '').trim();
|
|
240
|
-
const userId = asString((input as any)?.userId || '').trim();
|
|
241
|
-
if (!platform || !groupId || !userId) return null;
|
|
242
|
-
return { platform, groupId, userId };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function parseLegacySessionKeyToStrict(input: string): string | null {
|
|
246
|
-
const raw = asString(input).trim();
|
|
247
|
-
if (!raw) return null;
|
|
248
|
-
|
|
249
|
-
const directLegacy = raw.match(/^agent:main:bncr:direct:([0-9a-fA-F]+):0$/);
|
|
250
|
-
if (directLegacy?.[1]) {
|
|
251
|
-
const route = parseRouteFromHexScope(directLegacy[1].toLowerCase());
|
|
252
|
-
if (route) return buildFallbackSessionKey(route);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const bncrLegacy = raw.match(/^bncr:([0-9a-fA-F]+):0$/);
|
|
256
|
-
if (bncrLegacy?.[1]) {
|
|
257
|
-
const route = parseRouteFromHexScope(bncrLegacy[1].toLowerCase());
|
|
258
|
-
if (route) return buildFallbackSessionKey(route);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
const agentLegacy = raw.match(/^agent:main:bncr:([0-9a-fA-F]+):0$/);
|
|
262
|
-
if (agentLegacy?.[1]) {
|
|
263
|
-
const route = parseRouteFromHexScope(agentLegacy[1].toLowerCase());
|
|
264
|
-
if (route) return buildFallbackSessionKey(route);
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
if (isLowerHex(raw.toLowerCase())) {
|
|
268
|
-
const route = parseRouteFromHexScope(raw.toLowerCase());
|
|
269
|
-
if (route) return buildFallbackSessionKey(route);
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
return null;
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
function isLegacyNoiseRoute(route: BncrRoute): boolean {
|
|
276
|
-
const platform = asString(route.platform).trim().toLowerCase();
|
|
277
|
-
const groupId = asString(route.groupId).trim().toLowerCase();
|
|
278
|
-
const userId = asString(route.userId).trim().toLowerCase();
|
|
279
|
-
|
|
280
|
-
// 明确排除历史污染:agent:main:bncr(不是实际外部会话路由)
|
|
281
|
-
if (platform === 'agent' && groupId === 'main' && userId === 'bncr') return true;
|
|
282
|
-
|
|
283
|
-
// 明确排除嵌套遗留:bncr:<hex>:0(非真实外部 peer)
|
|
284
|
-
if (platform === 'bncr' && userId === '0' && isLowerHex(groupId)) return true;
|
|
285
|
-
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
function normalizeStoredSessionKey(input: string): { sessionKey: string; route: BncrRoute } | null {
|
|
290
|
-
const raw = asString(input).trim();
|
|
291
|
-
if (!raw) return null;
|
|
292
|
-
|
|
293
|
-
let taskKey: string | null = null;
|
|
294
|
-
let base = raw;
|
|
295
|
-
|
|
296
|
-
const taskTagged = raw.match(/^(.*):task:([a-z0-9_-]{1,32})$/i);
|
|
297
|
-
if (taskTagged) {
|
|
298
|
-
base = asString(taskTagged[1]).trim();
|
|
299
|
-
taskKey = normalizeTaskKey(taskTagged[2]);
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
const strict = parseStrictBncrSessionKey(base);
|
|
303
|
-
if (strict) {
|
|
304
|
-
if (isLegacyNoiseRoute(strict.route)) return null;
|
|
305
|
-
return {
|
|
306
|
-
sessionKey: taskKey ? `${strict.sessionKey}:task:${taskKey}` : strict.sessionKey,
|
|
307
|
-
route: strict.route,
|
|
308
|
-
};
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
const migrated = parseLegacySessionKeyToStrict(base);
|
|
312
|
-
if (!migrated) return null;
|
|
313
|
-
|
|
314
|
-
const parsed = parseStrictBncrSessionKey(migrated);
|
|
315
|
-
if (!parsed) return null;
|
|
316
|
-
if (isLegacyNoiseRoute(parsed.route)) return null;
|
|
317
|
-
|
|
318
|
-
return {
|
|
319
|
-
sessionKey: taskKey ? `${parsed.sessionKey}:task:${taskKey}` : parsed.sessionKey,
|
|
320
|
-
route: parsed.route,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
const BNCR_SESSION_KEY_PREFIX = 'agent:main:bncr:direct:';
|
|
325
|
-
|
|
326
|
-
function hex2utf8SessionKey(str: string): { sessionKey: string; scope: string } {
|
|
327
|
-
const raw = {
|
|
328
|
-
sessionKey: '',
|
|
329
|
-
scope: '',
|
|
330
|
-
};
|
|
331
|
-
if (!str) return raw;
|
|
332
|
-
|
|
333
|
-
const strarr = asString(str).trim().split(':');
|
|
334
|
-
const newarr: string[] = [];
|
|
335
|
-
|
|
336
|
-
for (const s of strarr) {
|
|
337
|
-
const part = asString(s).trim();
|
|
338
|
-
if (!part) {
|
|
339
|
-
newarr.push(part);
|
|
340
|
-
continue;
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
const decoded = Buffer.from(part, 'hex').toString('utf8');
|
|
344
|
-
if (decoded?.split(':')?.length === 3) {
|
|
345
|
-
newarr.push(decoded);
|
|
346
|
-
raw.scope = decoded;
|
|
347
|
-
} else {
|
|
348
|
-
newarr.push(part);
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
|
|
352
|
-
raw.sessionKey = newarr.join(':').trim();
|
|
353
|
-
return raw;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
function parseStrictBncrSessionKey(input: string): { sessionKey: string; scopeHex: string; route: BncrRoute } | null {
|
|
357
|
-
const raw = asString(input).trim();
|
|
358
|
-
if (!raw) return null;
|
|
359
|
-
|
|
360
|
-
// 终版兼容 sessionKey:
|
|
361
|
-
// 1) agent:main:bncr:direct:<hex(scope)>
|
|
362
|
-
// 2) agent:main:bncr:group:<hex(scope)>
|
|
363
|
-
// (统一归一成 direct:<hex(scope)>)
|
|
364
|
-
const m = raw.match(/^agent:main:bncr:(direct|group):(.+)$/);
|
|
365
|
-
if (!m?.[1] || !m?.[2]) return null;
|
|
366
|
-
|
|
367
|
-
const payload = asString(m[2]).trim();
|
|
368
|
-
let route: BncrRoute | null = null;
|
|
369
|
-
let scopeHex = '';
|
|
370
|
-
|
|
371
|
-
if (isLowerHex(payload)) {
|
|
372
|
-
scopeHex = payload;
|
|
373
|
-
route = parseRouteFromHexScope(payload);
|
|
374
|
-
} else {
|
|
375
|
-
route = parseRouteFromScope(payload);
|
|
376
|
-
if (route) scopeHex = routeScopeToHex(route);
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
if (!route || !scopeHex) return null;
|
|
380
|
-
|
|
381
|
-
return {
|
|
382
|
-
sessionKey: `${BNCR_SESSION_KEY_PREFIX}${scopeHex}`,
|
|
383
|
-
scopeHex,
|
|
384
|
-
route,
|
|
385
|
-
};
|
|
386
|
-
}
|
|
387
|
-
|
|
388
|
-
function normalizeInboundSessionKey(scope: string, route: BncrRoute): string | null {
|
|
389
|
-
const raw = asString(scope).trim();
|
|
390
|
-
if (!raw) return buildFallbackSessionKey(route);
|
|
391
|
-
|
|
392
|
-
const parsed = parseStrictBncrSessionKey(raw);
|
|
393
|
-
if (!parsed) return null;
|
|
394
|
-
return parsed.sessionKey;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function normalizeTaskKey(input: unknown): string | null {
|
|
398
|
-
const raw = asString(input).trim().toLowerCase();
|
|
399
|
-
if (!raw) return null;
|
|
400
|
-
|
|
401
|
-
const normalized = raw.replace(/[^a-z0-9_-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 32);
|
|
402
|
-
return normalized || null;
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
function extractInlineTaskKey(text: string): { taskKey: string | null; text: string } {
|
|
406
|
-
const raw = asString(text);
|
|
407
|
-
if (!raw) return { taskKey: null, text: '' };
|
|
408
|
-
|
|
409
|
-
// 形如:#task:xxx 或 /task:xxx
|
|
410
|
-
const tagged = raw.match(/^\s*(?:#task|\/task)\s*[:=]\s*([a-zA-Z0-9_-]{1,32})\s*\n?\s*([\s\S]*)$/i);
|
|
411
|
-
if (tagged) {
|
|
412
|
-
return {
|
|
413
|
-
taskKey: normalizeTaskKey(tagged[1]),
|
|
414
|
-
text: asString(tagged[2]),
|
|
415
|
-
};
|
|
416
|
-
}
|
|
417
|
-
|
|
418
|
-
// 形如:/task xxx 后跟正文
|
|
419
|
-
const spaced = raw.match(/^\s*\/task\s+([a-zA-Z0-9_-]{1,32})\s+([\s\S]*)$/i);
|
|
420
|
-
if (spaced) {
|
|
421
|
-
return {
|
|
422
|
-
taskKey: normalizeTaskKey(spaced[1]),
|
|
423
|
-
text: asString(spaced[2]),
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
return { taskKey: null, text: raw };
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
function withTaskSessionKey(sessionKey: string, taskKey?: string | null): string {
|
|
431
|
-
const base = asString(sessionKey).trim();
|
|
432
|
-
const tk = normalizeTaskKey(taskKey);
|
|
433
|
-
if (!base || !tk) return base;
|
|
434
|
-
|
|
435
|
-
if (/:task:[a-z0-9_-]+(?:$|:)/i.test(base)) return base;
|
|
436
|
-
return `${base}:task:${tk}`;
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function buildFallbackSessionKey(route: BncrRoute): string {
|
|
440
|
-
// 新主口径:sessionKey 使用 agent:main:bncr:direct:<hex(scope)>
|
|
441
|
-
return `${BNCR_SESSION_KEY_PREFIX}${routeScopeToHex(route)}`;
|
|
442
|
-
}
|
|
443
142
|
|
|
444
143
|
function backoffMs(retryCount: number): number {
|
|
445
144
|
// 1s,2s,4s,8s... capped by retry count checks
|
|
446
145
|
return Math.max(1_000, 1_000 * 2 ** Math.max(0, retryCount - 1));
|
|
447
146
|
}
|
|
448
147
|
|
|
449
|
-
function inboundDedupKey(params: {
|
|
450
|
-
accountId: string;
|
|
451
|
-
platform: string;
|
|
452
|
-
groupId: string;
|
|
453
|
-
userId: string;
|
|
454
|
-
msgId?: string;
|
|
455
|
-
text?: string;
|
|
456
|
-
mediaBase64?: string;
|
|
457
|
-
}): string {
|
|
458
|
-
const accountId = normalizeAccountId(params.accountId);
|
|
459
|
-
const platform = asString(params.platform).trim().toLowerCase();
|
|
460
|
-
const groupId = asString(params.groupId).trim();
|
|
461
|
-
const userId = asString(params.userId).trim();
|
|
462
|
-
const msgId = asString(params.msgId || '').trim();
|
|
463
|
-
|
|
464
|
-
if (msgId) return `${accountId}|${platform}|${groupId}|${userId}|msg:${msgId}`;
|
|
465
|
-
|
|
466
|
-
const text = asString(params.text || '').trim();
|
|
467
|
-
const media = asString(params.mediaBase64 || '');
|
|
468
|
-
const digest = createHash('sha1').update(`${text}\n${media.slice(0, 256)}`).digest('hex').slice(0, 16);
|
|
469
|
-
return `${accountId}|${platform}|${groupId}|${userId}|hash:${digest}`;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
function resolveChatType(_route: BncrRoute): 'direct' | 'group' {
|
|
473
|
-
// 业务口径:无论群聊/私聊,统一按 direct 上报,避免会话层落到 group 显示分支(历史 bncr:g-*)。
|
|
474
|
-
return 'direct';
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
function routeKey(accountId: string, route: BncrRoute): string {
|
|
478
|
-
return `${accountId}:${route.platform}:${route.groupId}:${route.userId}`.toLowerCase();
|
|
479
|
-
}
|
|
480
148
|
|
|
481
149
|
function fileExtFromMime(mimeType?: string): string {
|
|
482
150
|
const mt = asString(mimeType || '').toLowerCase();
|
|
@@ -530,24 +198,6 @@ function resolveOutboundFileName(params: { mediaUrl?: string; fileName?: string;
|
|
|
530
198
|
return `${stem}${ext}`;
|
|
531
199
|
}
|
|
532
200
|
|
|
533
|
-
function resolveBncrOutboundMessageType(params: { mimeType?: string; fileName?: string; hintedType?: string; hasPayload?: boolean }): 'text' | 'image' | 'video' | 'voice' | 'audio' | 'file' {
|
|
534
|
-
const hinted = asString(params.hintedType || '').toLowerCase();
|
|
535
|
-
const hasPayload = !!params.hasPayload;
|
|
536
|
-
const mt = asString(params.mimeType || '').toLowerCase();
|
|
537
|
-
const major = mt.split('/')[0] || '';
|
|
538
|
-
const isStandard = hinted === 'text' || hinted === 'image' || hinted === 'video' || hinted === 'voice' || hinted === 'audio' || hinted === 'file';
|
|
539
|
-
|
|
540
|
-
// 文本类附件不应落成 text:当上游显式给 text,或上游 type 不在标准列表时,若带文件载荷且 mime 主类型为 text,则归到 file。
|
|
541
|
-
if (hasPayload && major === 'text' && (hinted === 'text' || !isStandard)) return 'file';
|
|
542
|
-
|
|
543
|
-
// 优先使用上游已给出的标准类型;仅当不在支持列表时再尝试纠正
|
|
544
|
-
if (isStandard) return hinted as any;
|
|
545
|
-
|
|
546
|
-
if (major === 'text' || major === 'image' || major === 'video' || major === 'audio') return major as any;
|
|
547
|
-
|
|
548
|
-
return 'file';
|
|
549
|
-
}
|
|
550
|
-
|
|
551
201
|
class BncrBridgeRuntime {
|
|
552
202
|
private api: OpenClawPluginApi;
|
|
553
203
|
private statePath: string | null = null;
|
|
@@ -668,42 +318,26 @@ class BncrBridgeRuntime {
|
|
|
668
318
|
|
|
669
319
|
private buildIntegratedDiagnostics(accountId: string) {
|
|
670
320
|
const acc = normalizeAccountId(accountId);
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
const legacyAccountResidue = this.countLegacyAccountResidue(acc);
|
|
677
|
-
|
|
678
|
-
const totalKnownRoutes = Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc).length;
|
|
679
|
-
const connected = this.isOnline(acc);
|
|
680
|
-
|
|
681
|
-
const pluginIndexExists = fs.existsSync(path.join(process.cwd(), 'plugins', 'bncr', 'index.ts'));
|
|
682
|
-
const pluginChannelExists = fs.existsSync(path.join(process.cwd(), 'plugins', 'bncr', 'src', 'channel.ts'));
|
|
683
|
-
|
|
684
|
-
const health = {
|
|
685
|
-
connected,
|
|
686
|
-
pending,
|
|
687
|
-
deadLetter: dead,
|
|
321
|
+
return buildIntegratedDiagnosticsFromRuntime({
|
|
322
|
+
accountId: acc,
|
|
323
|
+
connected: this.isOnline(acc),
|
|
324
|
+
pending: Array.from(this.outbox.values()).filter((v) => v.accountId === acc).length,
|
|
325
|
+
deadLetter: this.deadLetter.filter((v) => v.accountId === acc).length,
|
|
688
326
|
activeConnections: this.activeConnectionCount(acc),
|
|
689
327
|
connectEvents: this.getCounter(this.connectEventsByAccount, acc),
|
|
690
328
|
inboundEvents: this.getCounter(this.inboundEventsByAccount, acc),
|
|
691
329
|
activityEvents: this.getCounter(this.activityEventsByAccount, acc),
|
|
692
330
|
ackEvents: this.getCounter(this.ackEventsByAccount, acc),
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
ok: invalidOutboxSessionKeys === 0 && legacyAccountResidue === 0,
|
|
704
|
-
};
|
|
705
|
-
|
|
706
|
-
return { health, regression };
|
|
331
|
+
startedAt: this.startedAt,
|
|
332
|
+
lastSession: this.lastSessionByAccount.get(acc) || null,
|
|
333
|
+
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
334
|
+
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
335
|
+
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
336
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc).length,
|
|
337
|
+
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
338
|
+
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
339
|
+
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
340
|
+
});
|
|
707
341
|
}
|
|
708
342
|
|
|
709
343
|
private async loadState() {
|
|
@@ -1204,10 +838,10 @@ class BncrBridgeRuntime {
|
|
|
1204
838
|
return alias?.route || parsed.route;
|
|
1205
839
|
}
|
|
1206
840
|
|
|
1207
|
-
//
|
|
1208
|
-
// 1)
|
|
1209
|
-
// 2)
|
|
1210
|
-
// 3)
|
|
841
|
+
// 严谨目标解析:
|
|
842
|
+
// 1) 标准 to 仅认 Bncr:<platform>:<groupId>:<userId> / Bncr:<platform>:<userId>
|
|
843
|
+
// 2) 仍接受 strict sessionKey 作为内部兼容输入
|
|
844
|
+
// 3) 其他旧格式直接失败,并输出标准格式提示日志
|
|
1211
845
|
private resolveVerifiedTarget(rawTarget: string, accountId: string): { sessionKey: string; route: BncrRoute; displayScope: string } {
|
|
1212
846
|
const acc = normalizeAccountId(accountId);
|
|
1213
847
|
const raw = asString(rawTarget).trim();
|
|
@@ -1226,9 +860,9 @@ class BncrBridgeRuntime {
|
|
|
1226
860
|
|
|
1227
861
|
if (!route) {
|
|
1228
862
|
this.api.logger.warn?.(
|
|
1229
|
-
`[bncr-target-invalid] raw=${raw} accountId=${acc} reason=unparseable-or-unknown standardTo=
|
|
863
|
+
`[bncr-target-invalid] raw=${raw} accountId=${acc} reason=unparseable-or-unknown standardTo=Bncr:<platform>:<groupId>:<userId>|Bncr:<platform>:<userId> standardSessionKey=agent:main:bncr:direct:<hex(scope)>`,
|
|
1230
864
|
);
|
|
1231
|
-
throw new Error(`bncr invalid target(standard:
|
|
865
|
+
throw new Error(`bncr invalid target(standard: Bncr:<platform>:<groupId>:<userId> | Bncr:<platform>:<userId>): ${raw}`);
|
|
1232
866
|
}
|
|
1233
867
|
|
|
1234
868
|
const wantedRouteKey = routeKey(acc, route);
|
|
@@ -1355,87 +989,77 @@ class BncrBridgeRuntime {
|
|
|
1355
989
|
return { path: finalPath, fileSha256: sha };
|
|
1356
990
|
}
|
|
1357
991
|
|
|
1358
|
-
private fmtAgo(ts?: number | null): string {
|
|
1359
|
-
if (!ts || !Number.isFinite(ts) || ts <= 0) return '-';
|
|
1360
|
-
const diff = Math.max(0, now() - ts);
|
|
1361
|
-
if (diff < 1_000) return 'just now';
|
|
1362
|
-
if (diff < 60_000) return `${Math.floor(diff / 1_000)}s ago`;
|
|
1363
|
-
if (diff < 3_600_000) return `${Math.floor(diff / 60_000)}m ago`;
|
|
1364
|
-
if (diff < 86_400_000) return `${Math.floor(diff / 3_600_000)}h ago`;
|
|
1365
|
-
return `${Math.floor(diff / 86_400_000)}d ago`;
|
|
1366
|
-
}
|
|
1367
|
-
|
|
1368
992
|
private buildStatusMeta(accountId: string) {
|
|
1369
993
|
const acc = normalizeAccountId(accountId);
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
lastActivityAt: lastActAt,
|
|
1391
|
-
lastActivityAgo,
|
|
1392
|
-
lastInboundAt,
|
|
1393
|
-
lastInboundAgo,
|
|
1394
|
-
lastOutboundAt,
|
|
1395
|
-
lastOutboundAgo,
|
|
1396
|
-
diagnostics,
|
|
1397
|
-
};
|
|
994
|
+
return buildStatusMetaFromRuntime({
|
|
995
|
+
accountId: acc,
|
|
996
|
+
connected: this.isOnline(acc),
|
|
997
|
+
pending: Array.from(this.outbox.values()).filter((v) => v.accountId === acc).length,
|
|
998
|
+
deadLetter: this.deadLetter.filter((v) => v.accountId === acc).length,
|
|
999
|
+
activeConnections: this.activeConnectionCount(acc),
|
|
1000
|
+
connectEvents: this.getCounter(this.connectEventsByAccount, acc),
|
|
1001
|
+
inboundEvents: this.getCounter(this.inboundEventsByAccount, acc),
|
|
1002
|
+
activityEvents: this.getCounter(this.activityEventsByAccount, acc),
|
|
1003
|
+
ackEvents: this.getCounter(this.ackEventsByAccount, acc),
|
|
1004
|
+
startedAt: this.startedAt,
|
|
1005
|
+
lastSession: this.lastSessionByAccount.get(acc) || null,
|
|
1006
|
+
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1007
|
+
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1008
|
+
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1009
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc).length,
|
|
1010
|
+
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1011
|
+
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1012
|
+
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
1013
|
+
});
|
|
1398
1014
|
}
|
|
1399
1015
|
|
|
1400
1016
|
getAccountRuntimeSnapshot(accountId: string) {
|
|
1401
1017
|
const acc = normalizeAccountId(accountId);
|
|
1402
|
-
|
|
1403
|
-
const lastEventAt = this.lastActivityByAccount.get(acc) || null;
|
|
1404
|
-
const lastInboundAt = this.lastInboundByAccount.get(acc) || null;
|
|
1405
|
-
const lastOutboundAt = this.lastOutboundByAccount.get(acc) || null;
|
|
1406
|
-
return {
|
|
1018
|
+
return buildAccountRuntimeSnapshot({
|
|
1407
1019
|
accountId: acc,
|
|
1020
|
+
connected: this.isOnline(acc),
|
|
1021
|
+
pending: Array.from(this.outbox.values()).filter((v) => v.accountId === acc).length,
|
|
1022
|
+
deadLetter: this.deadLetter.filter((v) => v.accountId === acc).length,
|
|
1023
|
+
activeConnections: this.activeConnectionCount(acc),
|
|
1024
|
+
connectEvents: this.getCounter(this.connectEventsByAccount, acc),
|
|
1025
|
+
inboundEvents: this.getCounter(this.inboundEventsByAccount, acc),
|
|
1026
|
+
activityEvents: this.getCounter(this.activityEventsByAccount, acc),
|
|
1027
|
+
ackEvents: this.getCounter(this.ackEventsByAccount, acc),
|
|
1028
|
+
startedAt: this.startedAt,
|
|
1029
|
+
lastSession: this.lastSessionByAccount.get(acc) || null,
|
|
1030
|
+
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1031
|
+
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1032
|
+
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1033
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc).length,
|
|
1034
|
+
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1035
|
+
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1408
1036
|
running: true,
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
lastEventAt,
|
|
1412
|
-
lastInboundAt,
|
|
1413
|
-
lastOutboundAt,
|
|
1414
|
-
// 状态映射:在线=linked,离线=configured
|
|
1415
|
-
mode: connected ? 'linked' : 'configured',
|
|
1416
|
-
meta: this.buildStatusMeta(acc),
|
|
1417
|
-
};
|
|
1037
|
+
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
1038
|
+
});
|
|
1418
1039
|
}
|
|
1419
1040
|
|
|
1420
1041
|
private buildStatusHeadline(accountId: string): string {
|
|
1421
1042
|
const acc = normalizeAccountId(accountId);
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1043
|
+
return buildStatusHeadlineFromRuntime({
|
|
1044
|
+
accountId: acc,
|
|
1045
|
+
connected: this.isOnline(acc),
|
|
1046
|
+
pending: Array.from(this.outbox.values()).filter((v) => v.accountId === acc).length,
|
|
1047
|
+
deadLetter: this.deadLetter.filter((v) => v.accountId === acc).length,
|
|
1048
|
+
activeConnections: this.activeConnectionCount(acc),
|
|
1049
|
+
connectEvents: this.getCounter(this.connectEventsByAccount, acc),
|
|
1050
|
+
inboundEvents: this.getCounter(this.inboundEventsByAccount, acc),
|
|
1051
|
+
activityEvents: this.getCounter(this.activityEventsByAccount, acc),
|
|
1052
|
+
ackEvents: this.getCounter(this.ackEventsByAccount, acc),
|
|
1053
|
+
startedAt: this.startedAt,
|
|
1054
|
+
lastSession: this.lastSessionByAccount.get(acc) || null,
|
|
1055
|
+
lastActivityAt: this.lastActivityByAccount.get(acc) || null,
|
|
1056
|
+
lastInboundAt: this.lastInboundByAccount.get(acc) || null,
|
|
1057
|
+
lastOutboundAt: this.lastOutboundByAccount.get(acc) || null,
|
|
1058
|
+
sessionRoutesCount: Array.from(this.sessionRoutes.values()).filter((v) => v.accountId === acc).length,
|
|
1059
|
+
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(acc),
|
|
1060
|
+
legacyAccountResidue: this.countLegacyAccountResidue(acc),
|
|
1061
|
+
channelRoot: path.join(process.cwd(), 'plugins', 'bncr'),
|
|
1062
|
+
});
|
|
1439
1063
|
}
|
|
1440
1064
|
|
|
1441
1065
|
getStatusHeadline(accountId: string): string {
|
|
@@ -1762,33 +1386,20 @@ class BncrBridgeRuntime {
|
|
|
1762
1386
|
});
|
|
1763
1387
|
const messageId = randomUUID();
|
|
1764
1388
|
const mediaMsg = first ? asString(payload.text || '') : '';
|
|
1765
|
-
const frame = {
|
|
1766
|
-
type: 'message.outbound',
|
|
1389
|
+
const frame = buildBncrMediaOutboundFrame({
|
|
1767
1390
|
messageId,
|
|
1768
|
-
idempotencyKey: messageId,
|
|
1769
1391
|
sessionKey,
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
path: media.path || mediaUrl,
|
|
1782
|
-
base64: media.mediaBase64 || '',
|
|
1783
|
-
fileName: resolveOutboundFileName({
|
|
1784
|
-
mediaUrl,
|
|
1785
|
-
fileName: media.fileName,
|
|
1786
|
-
mimeType: media.mimeType,
|
|
1787
|
-
}),
|
|
1788
|
-
transferMode: media.mode,
|
|
1789
|
-
},
|
|
1790
|
-
ts: now(),
|
|
1791
|
-
};
|
|
1392
|
+
route,
|
|
1393
|
+
media,
|
|
1394
|
+
mediaUrl,
|
|
1395
|
+
mediaMsg,
|
|
1396
|
+
fileName: resolveOutboundFileName({
|
|
1397
|
+
mediaUrl,
|
|
1398
|
+
fileName: media.fileName,
|
|
1399
|
+
mimeType: media.mimeType,
|
|
1400
|
+
}),
|
|
1401
|
+
now: now(),
|
|
1402
|
+
});
|
|
1792
1403
|
|
|
1793
1404
|
this.enqueueOutbound({
|
|
1794
1405
|
messageId,
|
|
@@ -1939,14 +1550,33 @@ class BncrBridgeRuntime {
|
|
|
1939
1550
|
|
|
1940
1551
|
handleDiagnostics = async ({ params, respond }: GatewayRequestHandlerOptions) => {
|
|
1941
1552
|
const accountId = normalizeAccountId(asString(params?.accountId || ''));
|
|
1553
|
+
const cfg = await this.api.runtime.config.loadConfig();
|
|
1942
1554
|
const runtime = this.getAccountRuntimeSnapshot(accountId);
|
|
1943
1555
|
const diagnostics = this.buildIntegratedDiagnostics(accountId);
|
|
1556
|
+
const permissions = buildBncrPermissionSummary(cfg ?? {});
|
|
1557
|
+
const probe = probeBncrAccount({
|
|
1558
|
+
accountId,
|
|
1559
|
+
connected: Boolean(runtime?.connected),
|
|
1560
|
+
pending: Number(runtime?.meta?.pending ?? 0),
|
|
1561
|
+
deadLetter: Number(runtime?.meta?.deadLetter ?? 0),
|
|
1562
|
+
activeConnections: this.activeConnectionCount(accountId),
|
|
1563
|
+
invalidOutboxSessionKeys: this.countInvalidOutboxSessionKeys(accountId),
|
|
1564
|
+
legacyAccountResidue: this.countLegacyAccountResidue(accountId),
|
|
1565
|
+
lastActivityAt: runtime?.meta?.lastActivityAt ?? null,
|
|
1566
|
+
structure: {
|
|
1567
|
+
coreComplete: true,
|
|
1568
|
+
inboundComplete: true,
|
|
1569
|
+
outboundComplete: true,
|
|
1570
|
+
},
|
|
1571
|
+
});
|
|
1944
1572
|
|
|
1945
1573
|
respond(true, {
|
|
1946
1574
|
channel: CHANNEL_ID,
|
|
1947
1575
|
accountId,
|
|
1948
1576
|
runtime,
|
|
1949
1577
|
diagnostics,
|
|
1578
|
+
permissions,
|
|
1579
|
+
probe,
|
|
1950
1580
|
now: now(),
|
|
1951
1581
|
});
|
|
1952
1582
|
};
|
|
@@ -2231,7 +1861,8 @@ class BncrBridgeRuntime {
|
|
|
2231
1861
|
};
|
|
2232
1862
|
|
|
2233
1863
|
handleInbound = async ({ params, respond, client, context }: GatewayRequestHandlerOptions) => {
|
|
2234
|
-
const
|
|
1864
|
+
const parsed = parseBncrInboundParams(params);
|
|
1865
|
+
const { accountId, platform, groupId, userId, sessionKeyfromroute, route, text, msgType, mediaBase64, mediaPathFromTransfer, mimeType, fileName, msgId, dedupKey, peer, extracted } = parsed;
|
|
2235
1866
|
const connId = asString(client?.connId || '').trim() || `no-conn-${Date.now()}`;
|
|
2236
1867
|
const clientId = asString((params as any)?.clientId || '').trim() || undefined;
|
|
2237
1868
|
this.rememberGatewayContext(context);
|
|
@@ -2239,39 +1870,10 @@ class BncrBridgeRuntime {
|
|
|
2239
1870
|
this.markActivity(accountId);
|
|
2240
1871
|
this.incrementCounter(this.inboundEventsByAccount, accountId);
|
|
2241
1872
|
|
|
2242
|
-
const platform = asString(params?.platform || '').trim();
|
|
2243
|
-
const groupId = asString(params?.groupId || '0').trim() || '0';
|
|
2244
|
-
const userId = asString(params?.userId || '').trim();
|
|
2245
|
-
const sessionKeyfromroute = asString(params?.sessionKey || '').trim();
|
|
2246
|
-
|
|
2247
1873
|
if (!platform || (!userId && !groupId)) {
|
|
2248
1874
|
respond(false, { error: 'platform/groupId/userId required' });
|
|
2249
1875
|
return;
|
|
2250
1876
|
}
|
|
2251
|
-
|
|
2252
|
-
const route: BncrRoute = {
|
|
2253
|
-
platform,
|
|
2254
|
-
groupId,
|
|
2255
|
-
userId,
|
|
2256
|
-
};
|
|
2257
|
-
|
|
2258
|
-
const text = asString(params?.msg || '');
|
|
2259
|
-
const msgType = asString(params?.type || 'text') || 'text';
|
|
2260
|
-
const mediaBase64 = asString(params?.base64 || '');
|
|
2261
|
-
const mediaPathFromTransfer = asString(params?.path || '').trim();
|
|
2262
|
-
const mimeType = asString(params?.mimeType || '').trim() || undefined;
|
|
2263
|
-
const fileName = asString(params?.fileName || '').trim() || undefined;
|
|
2264
|
-
const msgId = asString(params?.msgId || '').trim() || undefined;
|
|
2265
|
-
|
|
2266
|
-
const dedupKey = inboundDedupKey({
|
|
2267
|
-
accountId,
|
|
2268
|
-
platform,
|
|
2269
|
-
groupId,
|
|
2270
|
-
userId,
|
|
2271
|
-
msgId,
|
|
2272
|
-
text,
|
|
2273
|
-
mediaBase64,
|
|
2274
|
-
});
|
|
2275
1877
|
if (this.markInboundDedupSeen(dedupKey)) {
|
|
2276
1878
|
respond(true, {
|
|
2277
1879
|
accepted: true,
|
|
@@ -2282,34 +1884,32 @@ class BncrBridgeRuntime {
|
|
|
2282
1884
|
return;
|
|
2283
1885
|
}
|
|
2284
1886
|
|
|
2285
|
-
const peer = {
|
|
2286
|
-
kind: resolveChatType(route),
|
|
2287
|
-
id: route.groupId === '0' ? route.userId : route.groupId,
|
|
2288
|
-
} as const;
|
|
2289
|
-
|
|
2290
1887
|
const cfg = await this.api.runtime.config.loadConfig();
|
|
2291
|
-
const
|
|
1888
|
+
const gate = checkBncrMessageGate({
|
|
1889
|
+
parsed,
|
|
2292
1890
|
cfg,
|
|
2293
|
-
|
|
2294
|
-
accountId,
|
|
2295
|
-
peer,
|
|
1891
|
+
account: resolveAccount(cfg, accountId),
|
|
2296
1892
|
});
|
|
1893
|
+
if (!gate.allowed) {
|
|
1894
|
+
respond(true, {
|
|
1895
|
+
accepted: false,
|
|
1896
|
+
accountId,
|
|
1897
|
+
msgId: msgId ?? null,
|
|
1898
|
+
reason: gate.reason,
|
|
1899
|
+
});
|
|
1900
|
+
return;
|
|
1901
|
+
}
|
|
2297
1902
|
|
|
2298
|
-
const baseSessionKey = normalizeInboundSessionKey(sessionKeyfromroute, route)
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
1903
|
+
const baseSessionKey = normalizeInboundSessionKey(sessionKeyfromroute, route)
|
|
1904
|
+
|| this.api.runtime.channel.routing.resolveAgentRoute({
|
|
1905
|
+
cfg,
|
|
1906
|
+
channel: CHANNEL_ID,
|
|
1907
|
+
accountId,
|
|
1908
|
+
peer,
|
|
1909
|
+
}).sessionKey;
|
|
2304
1910
|
const taskSessionKey = withTaskSessionKey(baseSessionKey, extracted.taskKey);
|
|
2305
1911
|
const sessionKey = taskSessionKey || baseSessionKey;
|
|
2306
1912
|
|
|
2307
|
-
this.rememberSessionRoute(baseSessionKey, accountId, route);
|
|
2308
|
-
if (taskSessionKey && taskSessionKey !== baseSessionKey) {
|
|
2309
|
-
this.rememberSessionRoute(taskSessionKey, accountId, route);
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
// 先回 ACK,后异步处理 AI 回复
|
|
2313
1913
|
respond(true, {
|
|
2314
1914
|
accepted: true,
|
|
2315
1915
|
accountId,
|
|
@@ -2318,114 +1918,22 @@ class BncrBridgeRuntime {
|
|
|
2318
1918
|
taskKey: extracted.taskKey ?? null,
|
|
2319
1919
|
});
|
|
2320
1920
|
|
|
2321
|
-
void (
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
2328
|
-
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
|
|
2336
|
-
|
|
2337
|
-
mediaPath = saved.path;
|
|
2338
|
-
} else if (mediaPathFromTransfer && fs.existsSync(mediaPathFromTransfer)) {
|
|
2339
|
-
mediaPath = mediaPathFromTransfer;
|
|
2340
|
-
}
|
|
2341
|
-
|
|
2342
|
-
const rawBody = agentText || (msgType === 'text' ? '' : `[${msgType}]`);
|
|
2343
|
-
const body = this.api.runtime.channel.reply.formatAgentEnvelope({
|
|
2344
|
-
channel: 'Bncr',
|
|
2345
|
-
from: `${platform}:${groupId}:${userId}`,
|
|
2346
|
-
timestamp: Date.now(),
|
|
2347
|
-
previousTimestamp: this.api.runtime.channel.session.readSessionUpdatedAt({
|
|
2348
|
-
storePath,
|
|
2349
|
-
sessionKey,
|
|
2350
|
-
}),
|
|
2351
|
-
envelope: this.api.runtime.channel.reply.resolveEnvelopeFormatOptions(cfg),
|
|
2352
|
-
body: rawBody,
|
|
2353
|
-
});
|
|
2354
|
-
|
|
2355
|
-
const displayTo = formatDisplayScope(route);
|
|
2356
|
-
// 全场景口径:SenderId 统一上报为 hex(scope)
|
|
2357
|
-
// 作为平台侧稳定身份键,避免退化为裸 userId。
|
|
2358
|
-
const senderIdForContext =
|
|
2359
|
-
parseStrictBncrSessionKey(baseSessionKey)?.scopeHex
|
|
2360
|
-
|| routeScopeToHex(route);
|
|
2361
|
-
const ctxPayload = this.api.runtime.channel.reply.finalizeInboundContext({
|
|
2362
|
-
Body: body,
|
|
2363
|
-
BodyForAgent: rawBody,
|
|
2364
|
-
RawBody: rawBody,
|
|
2365
|
-
CommandBody: rawBody,
|
|
2366
|
-
MediaPath: mediaPath,
|
|
2367
|
-
MediaType: mimeType,
|
|
2368
|
-
From: `${CHANNEL_ID}:${platform}:${groupId}:${userId}`,
|
|
2369
|
-
To: displayTo,
|
|
2370
|
-
SessionKey: sessionKey,
|
|
2371
|
-
AccountId: accountId,
|
|
2372
|
-
ChatType: peer.kind,
|
|
2373
|
-
ConversationLabel: displayTo,
|
|
2374
|
-
SenderId: senderIdForContext,
|
|
2375
|
-
Provider: CHANNEL_ID,
|
|
2376
|
-
Surface: CHANNEL_ID,
|
|
2377
|
-
MessageSid: msgId,
|
|
2378
|
-
Timestamp: Date.now(),
|
|
2379
|
-
OriginatingChannel: CHANNEL_ID,
|
|
2380
|
-
OriginatingTo: displayTo,
|
|
2381
|
-
});
|
|
2382
|
-
|
|
2383
|
-
await this.api.runtime.channel.session.recordInboundSession({
|
|
2384
|
-
storePath,
|
|
2385
|
-
sessionKey,
|
|
2386
|
-
ctx: ctxPayload,
|
|
2387
|
-
onRecordError: (err) => {
|
|
2388
|
-
this.api.logger.warn?.(`bncr: record session failed: ${String(err)}`);
|
|
2389
|
-
},
|
|
2390
|
-
});
|
|
2391
|
-
|
|
2392
|
-
// 记录真正的业务活动时间(入站已完成解析并落会话)
|
|
2393
|
-
const inboundAt = now();
|
|
2394
|
-
this.lastInboundByAccount.set(accountId, inboundAt);
|
|
2395
|
-
this.markActivity(accountId, inboundAt);
|
|
2396
|
-
this.scheduleSave();
|
|
2397
|
-
|
|
2398
|
-
await this.api.runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
2399
|
-
ctx: ctxPayload,
|
|
2400
|
-
cfg,
|
|
2401
|
-
// BNCR 侧仅推送最终回复,不要流式 block 推送
|
|
2402
|
-
replyOptions: {
|
|
2403
|
-
disableBlockStreaming: true,
|
|
2404
|
-
},
|
|
2405
|
-
dispatcherOptions: {
|
|
2406
|
-
deliver: async (
|
|
2407
|
-
payload: { text?: string; mediaUrl?: string; mediaUrls?: string[] },
|
|
2408
|
-
info?: { kind?: 'tool' | 'block' | 'final' },
|
|
2409
|
-
) => {
|
|
2410
|
-
// 过滤掉流式 block/tool,仅投递 final
|
|
2411
|
-
if (info?.kind && info.kind !== 'final') return;
|
|
2412
|
-
|
|
2413
|
-
await this.enqueueFromReply({
|
|
2414
|
-
accountId,
|
|
2415
|
-
sessionKey,
|
|
2416
|
-
route,
|
|
2417
|
-
payload,
|
|
2418
|
-
});
|
|
2419
|
-
},
|
|
2420
|
-
onError: (err: unknown) => {
|
|
2421
|
-
this.api.logger.error?.(`bncr reply failed: ${String(err)}`);
|
|
2422
|
-
},
|
|
2423
|
-
},
|
|
2424
|
-
});
|
|
2425
|
-
} catch (err) {
|
|
2426
|
-
this.api.logger.error?.(`bncr inbound process failed: ${String(err)}`);
|
|
2427
|
-
}
|
|
2428
|
-
})();
|
|
1921
|
+
void dispatchBncrInbound({
|
|
1922
|
+
api: this.api,
|
|
1923
|
+
channelId: CHANNEL_ID,
|
|
1924
|
+
cfg,
|
|
1925
|
+
parsed,
|
|
1926
|
+
rememberSessionRoute: (sessionKey, accountId, route) => this.rememberSessionRoute(sessionKey, accountId, route),
|
|
1927
|
+
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
1928
|
+
setInboundActivity: (accountId, at) => {
|
|
1929
|
+
this.lastInboundByAccount.set(accountId, at);
|
|
1930
|
+
this.markActivity(accountId, at);
|
|
1931
|
+
},
|
|
1932
|
+
scheduleSave: () => this.scheduleSave(),
|
|
1933
|
+
logger: this.api.logger,
|
|
1934
|
+
}).catch((err) => {
|
|
1935
|
+
this.api.logger.error?.(`bncr inbound process failed: ${String(err)}`);
|
|
1936
|
+
});
|
|
2429
1937
|
};
|
|
2430
1938
|
|
|
2431
1939
|
channelStartAccount = async (ctx: any) => {
|
|
@@ -2494,21 +2002,17 @@ class BncrBridgeRuntime {
|
|
|
2494
2002
|
);
|
|
2495
2003
|
}
|
|
2496
2004
|
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
this.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
|
|
2500
|
-
|
|
2501
|
-
await this.enqueueFromReply({
|
|
2005
|
+
return sendBncrText({
|
|
2006
|
+
channelId: CHANNEL_ID,
|
|
2502
2007
|
accountId,
|
|
2503
|
-
|
|
2504
|
-
|
|
2505
|
-
payload: {
|
|
2506
|
-
text: asString(ctx.text || ''),
|
|
2507
|
-
},
|
|
2008
|
+
to,
|
|
2009
|
+
text: asString(ctx.text || ''),
|
|
2508
2010
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2011
|
+
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2012
|
+
rememberSessionRoute: (sessionKey, accountId, route) => this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2013
|
+
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2014
|
+
createMessageId: () => randomUUID(),
|
|
2509
2015
|
});
|
|
2510
|
-
|
|
2511
|
-
return { channel: CHANNEL_ID, messageId: randomUUID(), chatId: verified.sessionKey };
|
|
2512
2016
|
};
|
|
2513
2017
|
|
|
2514
2018
|
channelSendMedia = async (ctx: any) => {
|
|
@@ -2535,57 +2039,21 @@ class BncrBridgeRuntime {
|
|
|
2535
2039
|
);
|
|
2536
2040
|
}
|
|
2537
2041
|
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
this.rememberSessionRoute(verified.sessionKey, accountId, verified.route);
|
|
2541
|
-
|
|
2542
|
-
await this.enqueueFromReply({
|
|
2042
|
+
return sendBncrMedia({
|
|
2043
|
+
channelId: CHANNEL_ID,
|
|
2543
2044
|
accountId,
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
text: asString(ctx.text || ''),
|
|
2548
|
-
mediaUrl: asString(ctx.mediaUrl || ''),
|
|
2549
|
-
},
|
|
2045
|
+
to,
|
|
2046
|
+
text: asString(ctx.text || ''),
|
|
2047
|
+
mediaUrl: asString(ctx.mediaUrl || ''),
|
|
2550
2048
|
mediaLocalRoots: ctx.mediaLocalRoots,
|
|
2049
|
+
resolveVerifiedTarget: (to, accountId) => this.resolveVerifiedTarget(to, accountId),
|
|
2050
|
+
rememberSessionRoute: (sessionKey, accountId, route) => this.rememberSessionRoute(sessionKey, accountId, route),
|
|
2051
|
+
enqueueFromReply: (args) => this.enqueueFromReply(args),
|
|
2052
|
+
createMessageId: () => randomUUID(),
|
|
2551
2053
|
});
|
|
2552
|
-
|
|
2553
|
-
return { channel: CHANNEL_ID, messageId: randomUUID(), chatId: verified.sessionKey };
|
|
2554
2054
|
};
|
|
2555
2055
|
}
|
|
2556
2056
|
|
|
2557
|
-
function resolveDefaultDisplayName(rawName: unknown, accountId: string): string {
|
|
2558
|
-
const raw = asString(rawName || '').trim();
|
|
2559
|
-
// 统一兜底:空名 / 与 accountId 重复 / 历史默认名 => Monitor
|
|
2560
|
-
if (!raw || raw === accountId || /^bncr$/i.test(raw) || /^status$/i.test(raw) || /^runtime$/i.test(raw)) return 'Monitor';
|
|
2561
|
-
return raw;
|
|
2562
|
-
}
|
|
2563
|
-
|
|
2564
|
-
function resolveAccount(cfg: any, accountId?: string | null) {
|
|
2565
|
-
const accounts = cfg?.channels?.[CHANNEL_ID]?.accounts || {};
|
|
2566
|
-
let key = normalizeAccountId(accountId);
|
|
2567
|
-
|
|
2568
|
-
// 若请求的 accountId 不存在(例如框架仍传 default),回退到首个已配置账号
|
|
2569
|
-
if (!accounts[key]) {
|
|
2570
|
-
const first = Object.keys(accounts)[0];
|
|
2571
|
-
if (first) key = first;
|
|
2572
|
-
}
|
|
2573
|
-
|
|
2574
|
-
const account = accounts[key] || {};
|
|
2575
|
-
const displayName = resolveDefaultDisplayName(account?.name, key);
|
|
2576
|
-
return {
|
|
2577
|
-
accountId: key,
|
|
2578
|
-
// accountId(default) 无法隐藏时,给稳定默认名,避免空名或 default(default)
|
|
2579
|
-
name: displayName,
|
|
2580
|
-
enabled: account?.enabled !== false,
|
|
2581
|
-
};
|
|
2582
|
-
}
|
|
2583
|
-
|
|
2584
|
-
function listAccountIds(cfg: any): string[] {
|
|
2585
|
-
const ids = Object.keys(cfg?.channels?.[CHANNEL_ID]?.accounts || {});
|
|
2586
|
-
return ids.length ? ids : [BNCR_DEFAULT_ACCOUNT_ID];
|
|
2587
|
-
}
|
|
2588
|
-
|
|
2589
2057
|
export function createBncrBridge(api: OpenClawPluginApi) {
|
|
2590
2058
|
return new BncrBridgeRuntime(api);
|
|
2591
2059
|
}
|
|
@@ -2605,7 +2073,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2605
2073
|
chatTypes: ['direct'] as ChatType[],
|
|
2606
2074
|
media: true,
|
|
2607
2075
|
reply: true,
|
|
2608
|
-
nativeCommands:
|
|
2076
|
+
nativeCommands: true,
|
|
2609
2077
|
},
|
|
2610
2078
|
messaging: {
|
|
2611
2079
|
// 接收任意标签输入;不在 normalize 阶段做格式门槛,统一下沉到发送前验证。
|
|
@@ -2617,7 +2085,7 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2617
2085
|
looksLikeId: (raw: string, normalized?: string) => {
|
|
2618
2086
|
return Boolean(asString(normalized || raw).trim());
|
|
2619
2087
|
},
|
|
2620
|
-
hint: '
|
|
2088
|
+
hint: 'Standard to=Bncr:<platform>:<group>:<user> or Bncr:<platform>:<user>; sessionKey keeps existing strict/legacy compatibility, canonical sessionKey=agent:main:bncr:direct:<hex>',
|
|
2621
2089
|
},
|
|
2622
2090
|
},
|
|
2623
2091
|
configSchema: BncrConfigSchema,
|
|
@@ -2632,7 +2100,10 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2632
2100
|
enabled,
|
|
2633
2101
|
allowTopLevel: true,
|
|
2634
2102
|
}),
|
|
2635
|
-
isEnabled: (account: any) =>
|
|
2103
|
+
isEnabled: (account: any, cfg: any) => {
|
|
2104
|
+
const policy = resolveBncrChannelPolicy(cfg?.channels?.[CHANNEL_ID] || {});
|
|
2105
|
+
return policy.enabled !== false && account?.enabled !== false;
|
|
2106
|
+
},
|
|
2636
2107
|
isConfigured: () => true,
|
|
2637
2108
|
describeAccount: (account: any) => {
|
|
2638
2109
|
const displayName = resolveDefaultDisplayName(account?.name, account?.accountId);
|
|
@@ -2670,6 +2141,27 @@ export function createBncrChannelPlugin(bridge: BncrBridgeRuntime) {
|
|
|
2670
2141
|
textChunkLimit: 4000,
|
|
2671
2142
|
sendText: bridge.channelSendText,
|
|
2672
2143
|
sendMedia: bridge.channelSendMedia,
|
|
2144
|
+
replyAction: async (ctx: any) => sendBncrReplyAction({
|
|
2145
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
2146
|
+
to: asString(ctx?.to || '').trim(),
|
|
2147
|
+
text: asString(ctx?.text || ''),
|
|
2148
|
+
replyToMessageId: asString(ctx?.replyToId || ctx?.replyToMessageId || '').trim() || undefined,
|
|
2149
|
+
sendText: async ({ accountId, to, text }) => bridge.channelSendText({ accountId, to, text }),
|
|
2150
|
+
}),
|
|
2151
|
+
deleteAction: async (ctx: any) => deleteBncrMessageAction({
|
|
2152
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
2153
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
2154
|
+
}),
|
|
2155
|
+
reactAction: async (ctx: any) => reactBncrMessageAction({
|
|
2156
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
2157
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
2158
|
+
emoji: asString(ctx?.emoji || '').trim(),
|
|
2159
|
+
}),
|
|
2160
|
+
editAction: async (ctx: any) => editBncrMessageAction({
|
|
2161
|
+
accountId: normalizeAccountId(ctx?.accountId),
|
|
2162
|
+
targetMessageId: asString(ctx?.messageId || ctx?.targetMessageId || '').trim(),
|
|
2163
|
+
text: asString(ctx?.text || ''),
|
|
2164
|
+
}),
|
|
2673
2165
|
},
|
|
2674
2166
|
status: {
|
|
2675
2167
|
defaultRuntime: createDefaultChannelRuntimeState(BNCR_DEFAULT_ACCOUNT_ID, {
|