openclaw-plugin-yuanbao 2.13.5 → 2.15.0
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/src/access/http/request.d.ts +6 -0
- package/dist/src/access/http/request.js +1 -1
- package/dist/src/access/ws/biz-codec.d.ts +7 -1
- package/dist/src/access/ws/biz-codec.js +24 -0
- package/dist/src/access/ws/client.d.ts +4 -1
- package/dist/src/access/ws/client.js +12 -1
- package/dist/src/access/ws/gateway.js +28 -0
- package/dist/src/access/ws/proto/biz.json +41 -0
- package/dist/src/access/ws/types.d.ts +12 -0
- package/dist/src/business/messaging/context.d.ts +4 -0
- package/dist/src/business/messaging/handlers/custom/forward-records-proto.d.ts +8 -0
- package/dist/src/business/messaging/handlers/custom/forward-records-proto.js +103 -0
- package/dist/src/business/messaging/handlers/custom/forward-records.d.ts +63 -0
- package/dist/src/business/messaging/handlers/custom/forward-records.js +169 -0
- package/dist/src/business/messaging/handlers/custom/index.js +28 -3
- package/dist/src/business/messaging/handlers/types.d.ts +2 -0
- package/dist/src/business/outbound/queue.d.ts +1 -0
- package/dist/src/business/outbound/queue.js +1 -1
- package/dist/src/business/pipeline/middlewares/build-context.js +2 -2
- package/dist/src/business/pipeline/middlewares/extract-content.js +10 -3
- package/dist/src/business/pipeline/middlewares/guard-command.js +21 -2
- package/dist/src/business/pipeline/middlewares/guard-group-command.js +27 -24
- package/dist/src/business/pipeline/middlewares/guard-special-command.js +6 -4
- package/dist/src/business/pipeline/types.d.ts +2 -0
- package/dist/src/dispatcher/debouncer/index.js +14 -1
- package/dist/src/infra/reply-classify.d.ts +19 -0
- package/dist/src/infra/reply-classify.js +27 -0
- package/dist/src/infra/transport.js +11 -12
- package/dist/src/types.d.ts +4 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +7 -2
- package/dist/src/business/messaging/chat-history.test.d.ts +0 -4
- package/dist/src/business/messaging/chat-history.test.js +0 -48
- package/dist/src/business/messaging/extract.test.d.ts +0 -4
- package/dist/src/business/messaging/extract.test.js +0 -71
- package/dist/src/business/messaging/handlers/custom.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/custom.test.js +0 -82
- package/dist/src/business/messaging/handlers/face.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/face.test.js +0 -59
- package/dist/src/business/messaging/handlers/image.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/image.test.js +0 -88
- package/dist/src/business/messaging/handlers/index.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/index.test.js +0 -72
- package/dist/src/business/messaging/handlers/text.test.d.ts +0 -6
- package/dist/src/business/messaging/handlers/text.test.js +0 -39
- package/dist/src/business/messaging/quote.test.d.ts +0 -4
- package/dist/src/business/messaging/quote.test.js +0 -118
- package/dist/src/business/messaging/targets.test.d.ts +0 -6
- package/dist/src/business/messaging/targets.test.js +0 -46
- package/dist/src/business/pipeline/engine.test.d.ts +0 -4
- package/dist/src/business/pipeline/engine.test.js +0 -194
- package/dist/src/business/pipeline/middlewares/build-context.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/build-context.test.js +0 -132
- package/dist/src/business/pipeline/middlewares/dispatch-reply.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/dispatch-reply.test.js +0 -212
- package/dist/src/business/pipeline/middlewares/download-media.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/download-media.test.js +0 -318
- package/dist/src/business/pipeline/middlewares/extract-content.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/extract-content.test.js +0 -92
- package/dist/src/business/pipeline/middlewares/guard-command.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/guard-command.test.js +0 -129
- package/dist/src/business/pipeline/middlewares/guard-group-command.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/guard-group-command.test.js +0 -122
- package/dist/src/business/pipeline/middlewares/guard-send-access.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/guard-send-access.test.js +0 -77
- package/dist/src/business/pipeline/middlewares/guard-special-command.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/guard-special-command.test.js +0 -149
- package/dist/src/business/pipeline/middlewares/prepare-sender.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/prepare-sender.test.js +0 -131
- package/dist/src/business/pipeline/middlewares/record-member.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/record-member.test.js +0 -64
- package/dist/src/business/pipeline/middlewares/resolve-mention.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/resolve-mention.test.js +0 -130
- package/dist/src/business/pipeline/middlewares/resolve-quote.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/resolve-quote.test.js +0 -149
- package/dist/src/business/pipeline/middlewares/resolve-route.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/resolve-route.test.js +0 -87
- package/dist/src/business/pipeline/middlewares/rewrite-body.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/rewrite-body.test.js +0 -107
- package/dist/src/business/pipeline/middlewares/skip-placeholder.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/skip-placeholder.test.js +0 -97
- package/dist/src/business/pipeline/middlewares/skip-self.test.d.ts +0 -4
- package/dist/src/business/pipeline/middlewares/skip-self.test.js +0 -34
- package/dist/src/business/pipeline/test-helpers/mock-ctx.d.ts +0 -25
- package/dist/src/business/pipeline/test-helpers/mock-ctx.js +0 -115
- package/dist/src/business/utils/media.test.d.ts +0 -5
- package/dist/src/business/utils/media.test.js +0 -218
- package/dist/src/business/utils/utils.test.d.ts +0 -6
- package/dist/src/business/utils/utils.test.js +0 -43
- package/dist/src/infra/cache/ttl-db.test.d.ts +0 -4
- package/dist/src/infra/cache/ttl-db.test.js +0 -55
|
@@ -44,6 +44,12 @@ export declare function getTokenStatus(accountId: string): {
|
|
|
44
44
|
status: "valid" | "expired" | "refreshing" | "none";
|
|
45
45
|
expiresAt: number | null;
|
|
46
46
|
};
|
|
47
|
+
export declare function computeSignature(params: {
|
|
48
|
+
nonce: string;
|
|
49
|
+
timestamp: string;
|
|
50
|
+
appKey: string;
|
|
51
|
+
appSecret: string;
|
|
52
|
+
}): string;
|
|
47
53
|
export declare function verifySignature(expected: string, actual: string): boolean;
|
|
48
54
|
export declare function getSignToken(account: ResolvedYuanbaoAccount, log?: Log): Promise<SignTokenData>;
|
|
49
55
|
export declare function forceRefreshSignToken(account: ResolvedYuanbaoAccount, log?: Log): Promise<SignTokenData>;
|
|
@@ -46,7 +46,7 @@ export function getTokenStatus(accountId) {
|
|
|
46
46
|
expiresAt: cached.expiresAt,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
|
-
function computeSignature(params) {
|
|
49
|
+
export function computeSignature(params) {
|
|
50
50
|
const plain = params.nonce + params.timestamp + params.appKey + params.appSecret;
|
|
51
51
|
return createHmac("sha256", params.appSecret).update(plain).digest("hex");
|
|
52
52
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** Business-layer Protobuf codec — encode/decode for business messages. */
|
|
2
2
|
import type { YuanbaoInboundMessage, YuanbaoMsgBodyElement } from "../../types.js";
|
|
3
|
-
import type { WsSendC2CMessageData, WsSendGroupMessageData, WsSendMessageResponse, WsSendPrivateHeartbeatData, WsSendGroupHeartbeatData, WsHeartbeatResponse, WsQueryGroupInfoData, WsQueryGroupInfoResponse, WsGetGroupMemberListData, WsGetGroupMemberListResponse, WsSyncInformationData, WsSyncInformationResponse } from "./types.js";
|
|
3
|
+
import type { WsSendC2CMessageData, WsSendGroupMessageData, WsSendMessageResponse, WsSendPrivateHeartbeatData, WsSendGroupHeartbeatData, WsHeartbeatResponse, WsQueryGroupInfoData, WsQueryGroupInfoResponse, WsGetGroupMemberListData, WsGetGroupMemberListResponse, WsSyncInformationData, WsSyncInformationResponse, WsQueryBotInfoResponse } from "./types.js";
|
|
4
4
|
export declare const BIZ_MSG_TYPES: {
|
|
5
5
|
readonly MsgContent: "trpc.yuanbao.yuanbao_conn.yuanbao_openclaw_proxy.MsgContent";
|
|
6
6
|
readonly MsgBodyElement: "trpc.yuanbao.yuanbao_conn.yuanbao_openclaw_proxy.MsgBodyElement";
|
|
@@ -19,6 +19,8 @@ export declare const BIZ_MSG_TYPES: {
|
|
|
19
19
|
readonly SendGroupHeartbeatRsp: "trpc.yuanbao.yuanbao_conn.yuanbao_openclaw_proxy.SendGroupHeartbeatRsp";
|
|
20
20
|
readonly SyncInformationReq: "trpc.yuanbao.yuanbao_conn.yuanbao_openclaw_proxy.SyncInformationReq";
|
|
21
21
|
readonly SyncInformationRsp: "trpc.yuanbao.yuanbao_conn.yuanbao_openclaw_proxy.SyncInformationRsp";
|
|
22
|
+
readonly QueryBotInfoReq: "trpc.yuanbao.yuanbao_conn.yuanbao_openclaw_proxy.QueryBotInfoReq";
|
|
23
|
+
readonly QueryBotInfoRsp: "trpc.yuanbao.yuanbao_conn.yuanbao_openclaw_proxy.QueryBotInfoRsp";
|
|
22
24
|
};
|
|
23
25
|
export declare function encodeBizPB(key: string, value: Record<string, unknown>): Uint8Array | null;
|
|
24
26
|
export declare function decodeBizPB(key: string, data: Uint8Array | ArrayBuffer): unknown;
|
|
@@ -64,3 +66,7 @@ export declare function decodeSendGroupHeartbeatRsp(data: Uint8Array | ArrayBuff
|
|
|
64
66
|
export declare function encodeSyncInformationReq(data: WsSyncInformationData): Uint8Array | null;
|
|
65
67
|
/** Decode SyncInformationRsp. */
|
|
66
68
|
export declare function decodeSyncInformationRsp(data: Uint8Array | ArrayBuffer, msgId: string): WsSyncInformationResponse | null;
|
|
69
|
+
/** Encode QueryBotInfoReq. */
|
|
70
|
+
export declare function encodeQueryBotInfoReq(botId: string): Uint8Array | null;
|
|
71
|
+
/** Decode QueryBotInfoRsp. */
|
|
72
|
+
export declare function decodeQueryBotInfoRsp(data: Uint8Array | ArrayBuffer, msgId: string): WsQueryBotInfoResponse | null;
|
|
@@ -29,6 +29,8 @@ export const BIZ_MSG_TYPES = {
|
|
|
29
29
|
SendGroupHeartbeatRsp: `${PKG}.SendGroupHeartbeatRsp`,
|
|
30
30
|
SyncInformationReq: `${PKG}.SyncInformationReq`,
|
|
31
31
|
SyncInformationRsp: `${PKG}.SyncInformationRsp`,
|
|
32
|
+
QueryBotInfoReq: `${PKG}.QueryBotInfoReq`,
|
|
33
|
+
QueryBotInfoRsp: `${PKG}.QueryBotInfoRsp`,
|
|
32
34
|
};
|
|
33
35
|
export function encodeBizPB(key, value) {
|
|
34
36
|
try {
|
|
@@ -72,6 +74,7 @@ export function toProtoMsgBody(elements) {
|
|
|
72
74
|
url: c.url,
|
|
73
75
|
fileSize: c.file_size,
|
|
74
76
|
fileName: c.file_name,
|
|
77
|
+
extMap: c.ext_map,
|
|
75
78
|
},
|
|
76
79
|
};
|
|
77
80
|
});
|
|
@@ -120,6 +123,9 @@ export function fromProtoMsgBody(elements) {
|
|
|
120
123
|
if (mc?.fileName) {
|
|
121
124
|
content.file_name = mc.fileName;
|
|
122
125
|
}
|
|
126
|
+
if (mc?.extMap && Object.keys(mc.extMap).length > 0) {
|
|
127
|
+
content.ext_map = mc.extMap;
|
|
128
|
+
}
|
|
123
129
|
return {
|
|
124
130
|
msg_type: el.msgType || "",
|
|
125
131
|
msg_content: content,
|
|
@@ -366,3 +372,21 @@ export function decodeSyncInformationRsp(data, msgId) {
|
|
|
366
372
|
msg: decoded.msg || "",
|
|
367
373
|
};
|
|
368
374
|
}
|
|
375
|
+
/** Encode QueryBotInfoReq. */
|
|
376
|
+
export function encodeQueryBotInfoReq(botId) {
|
|
377
|
+
return encodeBizPB(BIZ_MSG_TYPES.QueryBotInfoReq, { botId });
|
|
378
|
+
}
|
|
379
|
+
/** Decode QueryBotInfoRsp. */
|
|
380
|
+
export function decodeQueryBotInfoRsp(data, msgId) {
|
|
381
|
+
const decoded = decodeBizPB(BIZ_MSG_TYPES.QueryBotInfoRsp, data);
|
|
382
|
+
if (!decoded) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
return {
|
|
386
|
+
msgId,
|
|
387
|
+
code: decoded.code || 0,
|
|
388
|
+
msg: decoded.message || "",
|
|
389
|
+
botId: decoded.botInfo?.botId || "",
|
|
390
|
+
ownerId: decoded.botInfo?.encryptOwnerId || "",
|
|
391
|
+
};
|
|
392
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/** WebSocket client — connection management, auth, heartbeat, and auto-reconnect. */
|
|
2
2
|
import type { LogSink } from "../../logger.js";
|
|
3
|
-
import type { WsClientCallbacks, WsClientConfig, WsClientState, WsConnectionConfig, WsSendMessageResponse, WsSendC2CMessageData, WsSendGroupMessageData, WsSendPrivateHeartbeatData, WsSendGroupHeartbeatData, WsHeartbeatResponse, WsQueryGroupInfoData, WsQueryGroupInfoResponse, WsGetGroupMemberListData, WsGetGroupMemberListResponse, WsSyncInformationData, WsSyncInformationResponse } from "./types.js";
|
|
3
|
+
import type { WsClientCallbacks, WsClientConfig, WsClientState, WsConnectionConfig, WsSendMessageResponse, WsSendC2CMessageData, WsSendGroupMessageData, WsSendPrivateHeartbeatData, WsSendGroupHeartbeatData, WsHeartbeatResponse, WsQueryGroupInfoData, WsQueryGroupInfoResponse, WsGetGroupMemberListData, WsGetGroupMemberListResponse, WsSyncInformationData, WsSyncInformationResponse, WsQueryBotInfoResponse } from "./types.js";
|
|
4
4
|
/** Outbound business commands */
|
|
5
5
|
export declare const BIZ_CMD: {
|
|
6
6
|
/** Send C2C message */
|
|
@@ -12,6 +12,7 @@ export declare const BIZ_CMD: {
|
|
|
12
12
|
readonly SendPrivateHeartbeat: "send_private_heartbeat";
|
|
13
13
|
readonly SendGroupHeartbeat: "send_group_heartbeat";
|
|
14
14
|
readonly SyncInformation: "sync_information";
|
|
15
|
+
readonly QueryBotInfo: "query_bot_info";
|
|
15
16
|
};
|
|
16
17
|
/**
|
|
17
18
|
* Yuanbao WebSocket client.
|
|
@@ -66,6 +67,8 @@ export declare class YuanbaoWsClient {
|
|
|
66
67
|
sendGroupHeartbeat(data: WsSendGroupHeartbeatData): Promise<WsHeartbeatResponse>;
|
|
67
68
|
/** Sync information to the backend (command list, etc.). */
|
|
68
69
|
syncInformation(data: WsSyncInformationData): Promise<WsSyncInformationResponse>;
|
|
70
|
+
/** Query bot info (owner id, etc.) from the backend. */
|
|
71
|
+
queryBotInfo(botId: string): Promise<WsQueryBotInfoResponse>;
|
|
69
72
|
/** Establish WebSocket connection and bind event handlers. */
|
|
70
73
|
private doConnect;
|
|
71
74
|
private onMessage;
|
|
@@ -4,7 +4,7 @@ import WebSocket from "ws";
|
|
|
4
4
|
import { msgBodyDesensitization } from "../../business/utils/utils.js";
|
|
5
5
|
import { getPluginVersion, getOpenclawVersion, getOperationSystem } from "../../infra/env.js";
|
|
6
6
|
import { createLog } from "../../logger.js";
|
|
7
|
-
import { encodeSendC2CMessageReq, encodeSendGroupMessageReq, decodeSendMessageRsp, encodeSendPrivateHeartbeatReq, encodeSendGroupHeartbeatReq, decodeSendPrivateHeartbeatRsp, decodeSendGroupHeartbeatRsp, encodeQueryGroupInfoReq, decodeQueryGroupInfoRsp, encodeGetGroupMemberListReq, decodeGetGroupMemberListRsp, encodeSyncInformationReq, decodeSyncInformationRsp, } from "./biz-codec.js";
|
|
7
|
+
import { encodeSendC2CMessageReq, encodeSendGroupMessageReq, decodeSendMessageRsp, encodeSendPrivateHeartbeatReq, encodeSendGroupHeartbeatReq, decodeSendPrivateHeartbeatRsp, decodeSendGroupHeartbeatRsp, encodeQueryGroupInfoReq, decodeQueryGroupInfoRsp, encodeGetGroupMemberListReq, decodeGetGroupMemberListRsp, encodeSyncInformationReq, decodeSyncInformationRsp, encodeQueryBotInfoReq, decodeQueryBotInfoRsp, } from "./biz-codec.js";
|
|
8
8
|
import { decodeConnMsg, decodePB, buildAuthBindMsg, buildPingMsg, buildPushAck, buildBusinessConnMsg, PB_MSG_TYPES, CMD_TYPE, CMD, } from "./conn-codec.js";
|
|
9
9
|
const DEFAULT_RECONNECT_DELAYS = [1_000, 2_000, 5_000, 10_000, 30_000, 60_000];
|
|
10
10
|
const NO_RECONNECT_CLOSE_CODES = new Set([
|
|
@@ -42,6 +42,7 @@ export const BIZ_CMD = {
|
|
|
42
42
|
SendPrivateHeartbeat: "send_private_heartbeat",
|
|
43
43
|
SendGroupHeartbeat: "send_group_heartbeat",
|
|
44
44
|
SyncInformation: "sync_information",
|
|
45
|
+
QueryBotInfo: "query_bot_info",
|
|
45
46
|
};
|
|
46
47
|
const BIZ_MODULE = "yuanbao_openclaw_proxy";
|
|
47
48
|
/**
|
|
@@ -241,6 +242,15 @@ export class YuanbaoWsClient {
|
|
|
241
242
|
}
|
|
242
243
|
return this.sendAndWaitWith(BIZ_CMD.SyncInformation, BIZ_MODULE, encoded, decodeSyncInformationRsp);
|
|
243
244
|
}
|
|
245
|
+
/** Query bot info (owner id, etc.) from the backend. */
|
|
246
|
+
queryBotInfo(botId) {
|
|
247
|
+
this.log.debug("[bot-info] querying bot info", { botId });
|
|
248
|
+
const encoded = encodeQueryBotInfoReq(botId);
|
|
249
|
+
if (!encoded) {
|
|
250
|
+
return Promise.reject(new Error("Failed to encode QueryBotInfoReq"));
|
|
251
|
+
}
|
|
252
|
+
return this.sendAndWaitWith(BIZ_CMD.QueryBotInfo, BIZ_MODULE, encoded, decodeQueryBotInfoRsp);
|
|
253
|
+
}
|
|
244
254
|
/** Establish WebSocket connection and bind event handlers. */
|
|
245
255
|
doConnect() {
|
|
246
256
|
if (this.disposed) {
|
|
@@ -563,6 +573,7 @@ export class YuanbaoWsClient {
|
|
|
563
573
|
this.lastHeartbeatAt = Date.now();
|
|
564
574
|
this.sendBinary(binary);
|
|
565
575
|
this.log.debug("heartbeat sent");
|
|
576
|
+
this.scheduleNextPingCheck();
|
|
566
577
|
}
|
|
567
578
|
onPingResponse(head, data) {
|
|
568
579
|
this.heartbeatAckReceived = true;
|
|
@@ -35,6 +35,12 @@ export async function startYuanbaoWsGateway(params) {
|
|
|
35
35
|
error: String(err),
|
|
36
36
|
});
|
|
37
37
|
});
|
|
38
|
+
// Query and cache bot owner id (non-blocking)
|
|
39
|
+
queryAndCacheBotOwner(client, account, gwlog).catch((err) => {
|
|
40
|
+
gwlog.warn(`[${account.accountId}] failed to query bot owner (non-blocking)`, {
|
|
41
|
+
error: String(err),
|
|
42
|
+
});
|
|
43
|
+
});
|
|
38
44
|
},
|
|
39
45
|
onDispatch: (pushEvent) => {
|
|
40
46
|
gwlog.debug(`[${account.accountId}] WS push: cmd=${pushEvent.cmd}, type=${pushEvent.type}`);
|
|
@@ -325,6 +331,28 @@ function handleWsDispatchEvent(params) {
|
|
|
325
331
|
dlog.error(`[${account.accountId}][dispatch] WS ${isGroup ? "group " : ""} message handler failed: ${String(err)}`);
|
|
326
332
|
});
|
|
327
333
|
}
|
|
334
|
+
/**
|
|
335
|
+
* Query bot info (owner id) from the backend after connection is established,
|
|
336
|
+
* and cache the result on the account object for use in owner-gating middleware.
|
|
337
|
+
*
|
|
338
|
+
* Called once after auth-bind succeeds. Failures are non-fatal.
|
|
339
|
+
*/
|
|
340
|
+
async function queryAndCacheBotOwner(client, account, log) {
|
|
341
|
+
const botId = account.botId;
|
|
342
|
+
if (!botId) {
|
|
343
|
+
log.warn(`[${account.accountId}] queryAndCacheBotOwner: botId not set, skipping`);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
const rsp = await client.queryBotInfo(botId);
|
|
347
|
+
if (rsp.code !== 0) {
|
|
348
|
+
log.warn(`[${account.accountId}] queryBotInfo returned non-zero code: ${rsp.code}`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (rsp.ownerId) {
|
|
352
|
+
account.botOwnerId = rsp.ownerId;
|
|
353
|
+
log.info(`[${account.accountId}] bot owner cached: ownerId=${rsp.ownerId}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
328
356
|
/**
|
|
329
357
|
* Sync the command list to the backend after connection is established.
|
|
330
358
|
*
|
|
@@ -97,6 +97,11 @@
|
|
|
97
97
|
"fileName": {
|
|
98
98
|
"type": "string",
|
|
99
99
|
"id": 12
|
|
100
|
+
},
|
|
101
|
+
"extMap": {
|
|
102
|
+
"keyType": "string",
|
|
103
|
+
"type": "string",
|
|
104
|
+
"id": 999
|
|
100
105
|
}
|
|
101
106
|
}
|
|
102
107
|
},
|
|
@@ -539,6 +544,42 @@
|
|
|
539
544
|
"id": 2
|
|
540
545
|
}
|
|
541
546
|
}
|
|
547
|
+
},
|
|
548
|
+
"BotInfo": {
|
|
549
|
+
"fields": {
|
|
550
|
+
"botId": {
|
|
551
|
+
"type": "string",
|
|
552
|
+
"id": 1
|
|
553
|
+
},
|
|
554
|
+
"encryptOwnerId": {
|
|
555
|
+
"type": "string",
|
|
556
|
+
"id": 2
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
},
|
|
560
|
+
"QueryBotInfoReq": {
|
|
561
|
+
"fields": {
|
|
562
|
+
"botId": {
|
|
563
|
+
"type": "string",
|
|
564
|
+
"id": 1
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
"QueryBotInfoRsp": {
|
|
569
|
+
"fields": {
|
|
570
|
+
"code": {
|
|
571
|
+
"type": "int32",
|
|
572
|
+
"id": 1
|
|
573
|
+
},
|
|
574
|
+
"message": {
|
|
575
|
+
"type": "string",
|
|
576
|
+
"id": 2
|
|
577
|
+
},
|
|
578
|
+
"botInfo": {
|
|
579
|
+
"type": "BotInfo",
|
|
580
|
+
"id": 3
|
|
581
|
+
}
|
|
582
|
+
}
|
|
542
583
|
}
|
|
543
584
|
}
|
|
544
585
|
}
|
|
@@ -173,3 +173,15 @@ export type WsSyncInformationResponse = {
|
|
|
173
173
|
code: number;
|
|
174
174
|
msg: string;
|
|
175
175
|
};
|
|
176
|
+
/** Query bot info request */
|
|
177
|
+
export type WsQueryBotInfoData = {
|
|
178
|
+
bot_id: string;
|
|
179
|
+
};
|
|
180
|
+
/** Query bot info response */
|
|
181
|
+
export type WsQueryBotInfoResponse = {
|
|
182
|
+
msgId: string;
|
|
183
|
+
code: number;
|
|
184
|
+
msg: string;
|
|
185
|
+
botId: string;
|
|
186
|
+
ownerId: string;
|
|
187
|
+
};
|
|
@@ -7,6 +7,10 @@ import type { ResolvedYuanbaoAccount } from "../../types.js";
|
|
|
7
7
|
/** Message processing context */
|
|
8
8
|
export type MessageHandlerContext = {
|
|
9
9
|
groupCode?: string;
|
|
10
|
+
/** User ID/account that sent the message; used for ext_map key matching. */
|
|
11
|
+
fromAccount?: string;
|
|
12
|
+
/** Display name of the message sender; used by forwarded-record parsing. */
|
|
13
|
+
senderNickname?: string;
|
|
10
14
|
account: ResolvedYuanbaoAccount;
|
|
11
15
|
config: OpenClawConfig;
|
|
12
16
|
core: PluginRuntime;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protobuf decoder for WeChat forwarded chat-record payloads.
|
|
3
|
+
*
|
|
4
|
+
* The server stores `ForwardMsgData` in `MsgContent.ext_map` as a base64
|
|
5
|
+
* string whose decoded bytes are protobuf wire data.
|
|
6
|
+
*/
|
|
7
|
+
import type { ForwardMsgData } from "./forward-records.js";
|
|
8
|
+
export declare function decodeForwardMsgDataBase64(value: string): ForwardMsgData | undefined;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Protobuf decoder for WeChat forwarded chat-record payloads.
|
|
3
|
+
*
|
|
4
|
+
* The server stores `ForwardMsgData` in `MsgContent.ext_map` as a base64
|
|
5
|
+
* string whose decoded bytes are protobuf wire data.
|
|
6
|
+
*/
|
|
7
|
+
import protobuf from "protobufjs";
|
|
8
|
+
const FORWARD_PROTO_DESCRIPTOR = {
|
|
9
|
+
nested: {
|
|
10
|
+
ForwardMsgData: {
|
|
11
|
+
fields: {
|
|
12
|
+
sub_type: { type: "int32", id: 1 },
|
|
13
|
+
msg_begin_time: { type: "int64", id: 2 },
|
|
14
|
+
msg_end_time: { type: "int64", id: 3 },
|
|
15
|
+
nick_name: { type: "string", id: 4 },
|
|
16
|
+
msg: { rule: "repeated", type: "Msg", id: 5 },
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
Msg: {
|
|
20
|
+
fields: {
|
|
21
|
+
sender: { type: "string", id: 1 },
|
|
22
|
+
time: { type: "int64", id: 2 },
|
|
23
|
+
plainText: { type: "string", id: 3 },
|
|
24
|
+
msgContent: { rule: "repeated", type: "MsgContent", id: 4 },
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
MsgContent: {
|
|
28
|
+
fields: {
|
|
29
|
+
type: { type: "int32", id: 1 },
|
|
30
|
+
text: { type: "string", id: 2 },
|
|
31
|
+
multimedia: { rule: "repeated", type: "Multimedia", id: 3 },
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
Multimedia: {
|
|
35
|
+
fields: {
|
|
36
|
+
type: { type: "string", id: 1 },
|
|
37
|
+
url: { type: "string", id: 2 },
|
|
38
|
+
origin_url: { type: "string", id: 3 },
|
|
39
|
+
file_name: { type: "string", id: 4 },
|
|
40
|
+
size: { type: "int64", id: 5 },
|
|
41
|
+
width: { type: "int32", id: 6 },
|
|
42
|
+
height: { type: "int32", id: 7 },
|
|
43
|
+
style: { type: "string", id: 8 },
|
|
44
|
+
pendants: { rule: "repeated", type: "string", id: 9 },
|
|
45
|
+
created_time: { type: "int64", id: 10 },
|
|
46
|
+
index_url: { type: "string", id: 11 },
|
|
47
|
+
cover_url: { type: "string", id: 12 },
|
|
48
|
+
duration: { type: "int64", id: 13 },
|
|
49
|
+
guide_id: { type: "int32", id: 14 },
|
|
50
|
+
media_id: { type: "string", id: 15 },
|
|
51
|
+
extract: { type: "bool", id: 16 },
|
|
52
|
+
title: { type: "string", id: 17 },
|
|
53
|
+
content: { type: "string", id: 18 },
|
|
54
|
+
session_id: { type: "string", id: 19 },
|
|
55
|
+
question_id: { type: "string", id: 20 },
|
|
56
|
+
goods_trace_id: { type: "string", id: 21 },
|
|
57
|
+
cache_key: { type: "string", id: 22 },
|
|
58
|
+
chat_record_id: { type: "string", id: 23 },
|
|
59
|
+
doc_type: { type: "string", id: 24 },
|
|
60
|
+
image_base64: { type: "string", id: 25 },
|
|
61
|
+
parse_file_type: { type: "string", id: 26 },
|
|
62
|
+
parse_file_url: { type: "string", id: 27 },
|
|
63
|
+
sensitive: { type: "bool", id: 28 },
|
|
64
|
+
extra: { type: "string", id: 29 },
|
|
65
|
+
icon_url: { type: "string", id: 30 },
|
|
66
|
+
source: { type: "string", id: 31 },
|
|
67
|
+
app_id: { type: "string", id: 32 },
|
|
68
|
+
link_url: { type: "string", id: 33 },
|
|
69
|
+
msg_count: { type: "int32", id: 34 },
|
|
70
|
+
msg_time: { type: "int64", id: 35 },
|
|
71
|
+
forward_msg_id: { type: "string", id: 36 },
|
|
72
|
+
},
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
let forwardMsgDataType;
|
|
77
|
+
function getForwardMsgDataType() {
|
|
78
|
+
if (!forwardMsgDataType) {
|
|
79
|
+
forwardMsgDataType = protobuf.Root.fromJSON(FORWARD_PROTO_DESCRIPTOR).lookupType("ForwardMsgData");
|
|
80
|
+
}
|
|
81
|
+
return forwardMsgDataType;
|
|
82
|
+
}
|
|
83
|
+
export function decodeForwardMsgDataBase64(value) {
|
|
84
|
+
try {
|
|
85
|
+
const bytes = Buffer.from(value, "base64");
|
|
86
|
+
if (bytes.length === 0) {
|
|
87
|
+
return undefined;
|
|
88
|
+
}
|
|
89
|
+
const type = getForwardMsgDataType();
|
|
90
|
+
const decoded = type.decode(bytes);
|
|
91
|
+
const object = type.toObject(decoded, {
|
|
92
|
+
longs: String,
|
|
93
|
+
enums: Number,
|
|
94
|
+
bytes: String,
|
|
95
|
+
arrays: true,
|
|
96
|
+
objects: true,
|
|
97
|
+
});
|
|
98
|
+
return object && typeof object === "object" ? object : undefined;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat forwarded chat-record (elem_type 1009) parsing.
|
|
3
|
+
*
|
|
4
|
+
* A forwarded chat record carries a truncated summary in `msg_content.data.text`
|
|
5
|
+
* and the full structured detail in `msg_content.ext_map` (protobuf field 999).
|
|
6
|
+
* The ext_map value is a base64-encoded protobuf `ForwardMsgData`.
|
|
7
|
+
*
|
|
8
|
+
* This module turns that detail into readable chat-record lines and appends
|
|
9
|
+
* contained images/files to the shared `medias` list so the existing download
|
|
10
|
+
* pipeline fetches them — matching how imageHandler / fileHandler already behave.
|
|
11
|
+
*/
|
|
12
|
+
import type { ExtractTextFromMsgBodyResult } from "../types.js";
|
|
13
|
+
/** Multimedia entry inside a forwarded message content. */
|
|
14
|
+
export interface ForwardMultimedia {
|
|
15
|
+
type?: string;
|
|
16
|
+
url?: string;
|
|
17
|
+
origin_url?: string;
|
|
18
|
+
parse_file_url?: string;
|
|
19
|
+
link_url?: string;
|
|
20
|
+
file_name?: string;
|
|
21
|
+
media_id?: string;
|
|
22
|
+
doc_type?: string;
|
|
23
|
+
title?: string;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
/** One content fragment of a forwarded message. */
|
|
27
|
+
export interface ForwardMsgContent {
|
|
28
|
+
type?: number;
|
|
29
|
+
text?: string;
|
|
30
|
+
multimedia?: ForwardMultimedia[];
|
|
31
|
+
}
|
|
32
|
+
/** One message inside a forwarded chat record. */
|
|
33
|
+
export interface ForwardMsg {
|
|
34
|
+
sender?: string;
|
|
35
|
+
time?: number | string;
|
|
36
|
+
plainText?: string;
|
|
37
|
+
msgContent?: ForwardMsgContent[];
|
|
38
|
+
}
|
|
39
|
+
/** Parsed `ForwardMsgData` (ext_map value). */
|
|
40
|
+
export interface ForwardMsgData {
|
|
41
|
+
sub_type?: number;
|
|
42
|
+
msg_begin_time?: number | string;
|
|
43
|
+
msg_end_time?: number | string;
|
|
44
|
+
nick_name?: string;
|
|
45
|
+
msg?: ForwardMsg[];
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Extract the `ForwardMsgData` from a `msg_content.ext_map`.
|
|
49
|
+
*
|
|
50
|
+
* Matching strategy (per spec): prefer a `wexin_forward_msg_*` key that ends
|
|
51
|
+
* with `_{userId}`; otherwise fall back to the first entry that parses into a
|
|
52
|
+
* WeChat chat record (`sub_type === 1`). Returns undefined when none matches.
|
|
53
|
+
*/
|
|
54
|
+
export declare function parseForwardMsgData(extMap: Record<string, unknown> | undefined, userId?: string): ForwardMsgData | undefined;
|
|
55
|
+
/**
|
|
56
|
+
* Build a structured text block from a forwarded chat record and append any
|
|
57
|
+
* contained images/files to `resData.medias` (and link URLs to
|
|
58
|
+
* `resData.linkUrls`) for the downstream download / link-understanding steps.
|
|
59
|
+
*
|
|
60
|
+
* @param senderNickname display name of the user who forwarded the record.
|
|
61
|
+
* @returns the structured text, or undefined when the record has no messages.
|
|
62
|
+
*/
|
|
63
|
+
export declare function buildForwardRecordsText(data: ForwardMsgData, resData: ExtractTextFromMsgBodyResult, senderNickname?: string): string | undefined;
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WeChat forwarded chat-record (elem_type 1009) parsing.
|
|
3
|
+
*
|
|
4
|
+
* A forwarded chat record carries a truncated summary in `msg_content.data.text`
|
|
5
|
+
* and the full structured detail in `msg_content.ext_map` (protobuf field 999).
|
|
6
|
+
* The ext_map value is a base64-encoded protobuf `ForwardMsgData`.
|
|
7
|
+
*
|
|
8
|
+
* This module turns that detail into readable chat-record lines and appends
|
|
9
|
+
* contained images/files to the shared `medias` list so the existing download
|
|
10
|
+
* pipeline fetches them — matching how imageHandler / fileHandler already behave.
|
|
11
|
+
*/
|
|
12
|
+
import { sanitizeMediaFilename } from "../../../utils/media.js";
|
|
13
|
+
import { decodeForwardMsgDataBase64 } from "./forward-records-proto.js";
|
|
14
|
+
/** EnumMsgContentType inside a forwarded record. */
|
|
15
|
+
var ForwardContentType;
|
|
16
|
+
(function (ForwardContentType) {
|
|
17
|
+
ForwardContentType[ForwardContentType["Text"] = 1] = "Text";
|
|
18
|
+
ForwardContentType[ForwardContentType["Multimedia"] = 2] = "Multimedia";
|
|
19
|
+
ForwardContentType[ForwardContentType["ForwardMsg"] = 3] = "ForwardMsg";
|
|
20
|
+
})(ForwardContentType || (ForwardContentType = {}));
|
|
21
|
+
/** ext_map key prefix for WeChat forwarded chat records. */
|
|
22
|
+
const FORWARD_KEY_PREFIX = "wexin_forward_msg_";
|
|
23
|
+
/** Cap the number of records folded into a prompt to keep it bounded. */
|
|
24
|
+
const MAX_RECORDS = 100;
|
|
25
|
+
const HEADER_RECORDS = "以下为用户的聊天记录";
|
|
26
|
+
/**
|
|
27
|
+
* Extract the `ForwardMsgData` from a `msg_content.ext_map`.
|
|
28
|
+
*
|
|
29
|
+
* Matching strategy (per spec): prefer a `wexin_forward_msg_*` key that ends
|
|
30
|
+
* with `_{userId}`; otherwise fall back to the first entry that parses into a
|
|
31
|
+
* WeChat chat record (`sub_type === 1`). Returns undefined when none matches.
|
|
32
|
+
*/
|
|
33
|
+
export function parseForwardMsgData(extMap, userId) {
|
|
34
|
+
if (!extMap || typeof extMap !== "object") {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const entries = Object.entries(extMap).filter(([key]) => key.startsWith(FORWARD_KEY_PREFIX));
|
|
38
|
+
if (entries.length === 0) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
// Prefer a key whose suffix matches the current user; otherwise keep order.
|
|
42
|
+
const ordered = userId
|
|
43
|
+
? [...entries].sort(([a], [b]) => {
|
|
44
|
+
const am = a.endsWith(`_${userId}`) ? 0 : 1;
|
|
45
|
+
const bm = b.endsWith(`_${userId}`) ? 0 : 1;
|
|
46
|
+
return am - bm;
|
|
47
|
+
})
|
|
48
|
+
: entries;
|
|
49
|
+
for (const [, value] of ordered) {
|
|
50
|
+
const data = coerceForwardData(value);
|
|
51
|
+
if (data && Number(data.sub_type) === 1) {
|
|
52
|
+
return data;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
function coerceForwardData(value) {
|
|
58
|
+
if (typeof value === "string") {
|
|
59
|
+
return decodeForwardMsgDataBase64(value);
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Build a structured text block from a forwarded chat record and append any
|
|
65
|
+
* contained images/files to `resData.medias` (and link URLs to
|
|
66
|
+
* `resData.linkUrls`) for the downstream download / link-understanding steps.
|
|
67
|
+
*
|
|
68
|
+
* @param senderNickname display name of the user who forwarded the record.
|
|
69
|
+
* @returns the structured text, or undefined when the record has no messages.
|
|
70
|
+
*/
|
|
71
|
+
export function buildForwardRecordsText(data, resData, senderNickname) {
|
|
72
|
+
const msgList = Array.isArray(data.msg) ? data.msg.slice(0, MAX_RECORDS) : [];
|
|
73
|
+
if (msgList.length === 0) {
|
|
74
|
+
return undefined;
|
|
75
|
+
}
|
|
76
|
+
const lines = [];
|
|
77
|
+
if (senderNickname) {
|
|
78
|
+
lines.push(`当前用户的昵称为${senderNickname}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push(HEADER_RECORDS);
|
|
81
|
+
for (const msg of msgList) {
|
|
82
|
+
const sender = msg.sender ?? "";
|
|
83
|
+
const parts = buildMessageParts(msg, resData);
|
|
84
|
+
const body = parts.length > 0 ? parts.join(" ") : (msg.plainText ?? "");
|
|
85
|
+
lines.push(`${sender}:${body}`);
|
|
86
|
+
}
|
|
87
|
+
return lines.join("\n");
|
|
88
|
+
}
|
|
89
|
+
function buildMessageParts(msg, resData) {
|
|
90
|
+
const contents = Array.isArray(msg.msgContent) ? msg.msgContent : [];
|
|
91
|
+
if (contents.length === 0) {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
const parts = [];
|
|
95
|
+
for (const content of contents) {
|
|
96
|
+
switch (content.type) {
|
|
97
|
+
case ForwardContentType.Text:
|
|
98
|
+
if (content.text) {
|
|
99
|
+
parts.push(content.text);
|
|
100
|
+
}
|
|
101
|
+
break;
|
|
102
|
+
case ForwardContentType.Multimedia:
|
|
103
|
+
for (const media of content.multimedia ?? []) {
|
|
104
|
+
const part = appendMedia(media, resData);
|
|
105
|
+
if (part) {
|
|
106
|
+
parts.push(part);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
break;
|
|
110
|
+
case ForwardContentType.ForwardMsg:
|
|
111
|
+
parts.push("[嵌套聊天记录]");
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
if (msg.plainText) {
|
|
115
|
+
parts.push(msg.plainText);
|
|
116
|
+
}
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return parts;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Resolve a single multimedia item to a text placeholder and, for downloadable
|
|
124
|
+
* media (image/file/code/video-as-file), push it to `resData.medias`. Link
|
|
125
|
+
* shares are recorded in `resData.linkUrls` for link understanding.
|
|
126
|
+
*/
|
|
127
|
+
function appendMedia(media, resData) {
|
|
128
|
+
const mediaType = (media.type || media.doc_type || "").toLowerCase();
|
|
129
|
+
const url = media.url || media.origin_url || media.parse_file_url || media.link_url || "";
|
|
130
|
+
switch (mediaType) {
|
|
131
|
+
case "image": {
|
|
132
|
+
if (!url) {
|
|
133
|
+
return `[image:${media.file_name || "image"}]`;
|
|
134
|
+
}
|
|
135
|
+
const count = resData.medias.filter(m => m.mediaType === "image").length + 1;
|
|
136
|
+
const mediaName = sanitizeMediaFilename(media.file_name || media.media_id, `image${count}`);
|
|
137
|
+
resData.medias.push({ mediaType: "image", url, mediaName });
|
|
138
|
+
return `[image:${mediaName}]`;
|
|
139
|
+
}
|
|
140
|
+
case "file":
|
|
141
|
+
case "code":
|
|
142
|
+
case "document": {
|
|
143
|
+
if (!url) {
|
|
144
|
+
return `[file:${media.file_name || "file"}]`;
|
|
145
|
+
}
|
|
146
|
+
const count = resData.medias.filter(m => m.mediaType === "file").length + 1;
|
|
147
|
+
const mediaName = sanitizeMediaFilename(media.file_name || media.media_id, `file${count}`);
|
|
148
|
+
resData.medias.push({ mediaType: "file", url, mediaName });
|
|
149
|
+
return `[file:${mediaName}]`;
|
|
150
|
+
}
|
|
151
|
+
case "url": {
|
|
152
|
+
if (url) {
|
|
153
|
+
resData.linkUrls.push(url);
|
|
154
|
+
}
|
|
155
|
+
return `[link] ${[media.title || media.file_name, url].filter(Boolean).join(" ")}`.trimEnd();
|
|
156
|
+
}
|
|
157
|
+
case "video": {
|
|
158
|
+
if (!url) {
|
|
159
|
+
return `[video] ${media.file_name || "video"}`;
|
|
160
|
+
}
|
|
161
|
+
const count = resData.medias.filter(m => m.mediaType === "file").length + 1;
|
|
162
|
+
const mediaName = sanitizeMediaFilename(media.file_name || media.media_id, `video${count}`);
|
|
163
|
+
resData.medias.push({ mediaType: "file", url, mediaName });
|
|
164
|
+
return `[video:${mediaName}]`;
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
return `[${mediaType || "media"}] ${url || media.file_name || ""}`.trimEnd();
|
|
168
|
+
}
|
|
169
|
+
}
|