openclaw-plugin-yuanbao 2.13.4 → 2.14.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/index.d.ts +1 -1
- package/dist/setup-entry.d.ts +1 -1
- package/dist/src/access/ws/biz-codec.d.ts +7 -1
- package/dist/src/access/ws/biz-codec.js +20 -0
- package/dist/src/access/ws/client.d.ts +4 -1
- package/dist/src/access/ws/client.js +11 -1
- package/dist/src/access/ws/gateway.js +28 -0
- package/dist/src/access/ws/proto/biz.json +36 -0
- package/dist/src/access/ws/types.d.ts +12 -0
- package/dist/src/business/commands/log-upload/cos-upload.js +2 -39
- package/dist/src/business/commands/upgrade/utils.d.ts +1 -1
- package/dist/src/business/commands/upgrade/utils.js +15 -12
- package/dist/src/business/messaging/chat-history.d.ts +7 -2
- package/dist/src/business/messaging/chat-history.js +17 -7
- package/dist/src/business/messaging/extract.test.js +5 -3
- package/dist/src/business/messaging/handlers/file.js +5 -4
- package/dist/src/business/messaging/handlers/image.d.ts +2 -2
- package/dist/src/business/messaging/handlers/image.js +27 -8
- package/dist/src/business/messaging/handlers/image.test.js +36 -3
- package/dist/src/business/messaging/quote.d.ts +20 -11
- package/dist/src/business/messaging/quote.js +68 -28
- package/dist/src/business/messaging/quote.test.d.ts +1 -1
- package/dist/src/business/messaging/quote.test.js +52 -6
- package/dist/src/business/pipeline/middlewares/build-context.js +2 -2
- package/dist/src/business/pipeline/middlewares/dispatch-reply.js +2 -1
- package/dist/src/business/pipeline/middlewares/download-media.d.ts +15 -1
- package/dist/src/business/pipeline/middlewares/download-media.js +70 -49
- package/dist/src/business/pipeline/middlewares/download-media.test.d.ts +1 -1
- package/dist/src/business/pipeline/middlewares/download-media.test.js +218 -15
- 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-group-command.test.js +23 -46
- package/dist/src/business/pipeline/middlewares/guard-special-command.js +6 -4
- package/dist/src/business/pipeline/middlewares/resolve-mention.js +2 -2
- package/dist/src/business/pipeline/middlewares/resolve-mention.test.js +6 -0
- package/dist/src/business/pipeline/middlewares/resolve-quote.d.ts +3 -1
- package/dist/src/business/pipeline/middlewares/resolve-quote.js +6 -2
- package/dist/src/business/pipeline/middlewares/resolve-quote.test.d.ts +1 -1
- package/dist/src/business/pipeline/middlewares/resolve-quote.test.js +111 -13
- package/dist/src/business/pipeline/test-helpers/mock-ctx.js +1 -0
- package/dist/src/business/pipeline/types.d.ts +2 -0
- package/dist/src/business/utils/media.d.ts +17 -6
- package/dist/src/business/utils/media.js +80 -70
- package/dist/src/business/utils/media.test.d.ts +5 -0
- package/dist/src/business/utils/media.test.js +218 -0
- package/dist/src/dispatcher/debouncer/index.d.ts +1 -0
- package/dist/src/dispatcher/debouncer/index.js +14 -1
- package/dist/src/infra/cos.d.ts +20 -0
- package/dist/src/infra/cos.js +95 -0
- package/dist/src/types.d.ts +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("openclaw/plugin-sdk/channel-entry-contract").BundledChannelEntryContract<import("openclaw/plugin-sdk").ChannelPlugin>;
|
|
1
|
+
declare const _default: import("openclaw/plugin-sdk/channel-entry-contract").BundledChannelEntryContract<import("openclaw/plugin-sdk/channel-plugin-common").ChannelPlugin>;
|
|
2
2
|
export default _default;
|
package/dist/setup-entry.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
declare const _default: import("openclaw/plugin-sdk/channel-entry-contract").BundledChannelSetupEntryContract<import("openclaw/plugin-sdk").ChannelPlugin>;
|
|
1
|
+
declare const _default: import("openclaw/plugin-sdk/channel-entry-contract").BundledChannelSetupEntryContract<import("openclaw/plugin-sdk/channel-plugin-common").ChannelPlugin>;
|
|
2
2
|
export default _default;
|
|
@@ -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 {
|
|
@@ -366,3 +368,21 @@ export function decodeSyncInformationRsp(data, msgId) {
|
|
|
366
368
|
msg: decoded.msg || "",
|
|
367
369
|
};
|
|
368
370
|
}
|
|
371
|
+
/** Encode QueryBotInfoReq. */
|
|
372
|
+
export function encodeQueryBotInfoReq(botId) {
|
|
373
|
+
return encodeBizPB(BIZ_MSG_TYPES.QueryBotInfoReq, { botId });
|
|
374
|
+
}
|
|
375
|
+
/** Decode QueryBotInfoRsp. */
|
|
376
|
+
export function decodeQueryBotInfoRsp(data, msgId) {
|
|
377
|
+
const decoded = decodeBizPB(BIZ_MSG_TYPES.QueryBotInfoRsp, data);
|
|
378
|
+
if (!decoded) {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
return {
|
|
382
|
+
msgId,
|
|
383
|
+
code: decoded.code || 0,
|
|
384
|
+
msg: decoded.message || "",
|
|
385
|
+
botId: decoded.botInfo?.botId || "",
|
|
386
|
+
ownerId: decoded.botInfo?.encryptOwnerId || "",
|
|
387
|
+
};
|
|
388
|
+
}
|
|
@@ -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) {
|
|
@@ -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
|
*
|
|
@@ -539,6 +539,42 @@
|
|
|
539
539
|
"id": 2
|
|
540
540
|
}
|
|
541
541
|
}
|
|
542
|
+
},
|
|
543
|
+
"BotInfo": {
|
|
544
|
+
"fields": {
|
|
545
|
+
"botId": {
|
|
546
|
+
"type": "string",
|
|
547
|
+
"id": 1
|
|
548
|
+
},
|
|
549
|
+
"encryptOwnerId": {
|
|
550
|
+
"type": "string",
|
|
551
|
+
"id": 2
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
},
|
|
555
|
+
"QueryBotInfoReq": {
|
|
556
|
+
"fields": {
|
|
557
|
+
"botId": {
|
|
558
|
+
"type": "string",
|
|
559
|
+
"id": 1
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
},
|
|
563
|
+
"QueryBotInfoRsp": {
|
|
564
|
+
"fields": {
|
|
565
|
+
"code": {
|
|
566
|
+
"type": "int32",
|
|
567
|
+
"id": 1
|
|
568
|
+
},
|
|
569
|
+
"message": {
|
|
570
|
+
"type": "string",
|
|
571
|
+
"id": 2
|
|
572
|
+
},
|
|
573
|
+
"botInfo": {
|
|
574
|
+
"type": "BotInfo",
|
|
575
|
+
"id": 3
|
|
576
|
+
}
|
|
577
|
+
}
|
|
542
578
|
}
|
|
543
579
|
}
|
|
544
580
|
}
|
|
@@ -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
|
+
};
|
|
@@ -2,6 +2,7 @@ import { randomBytes } from "node:crypto";
|
|
|
2
2
|
import { readFile } from "node:fs/promises";
|
|
3
3
|
import { basename } from "node:path";
|
|
4
4
|
import { apiGetUploadInfo } from "../../../access/api.js";
|
|
5
|
+
import { createCosClient } from "../../../infra/cos.js";
|
|
5
6
|
import { createLog } from "../../../logger.js";
|
|
6
7
|
const DEFAULT_RECORD_API_URL = "https://yuanbao.tencent.com/e/api/clawLogUpload";
|
|
7
8
|
function resolveRecordApiUrl(config) {
|
|
@@ -14,46 +15,8 @@ function resolveRecordApiUrl(config) {
|
|
|
14
15
|
function generateFileId() {
|
|
15
16
|
return randomBytes(16).toString("hex");
|
|
16
17
|
}
|
|
17
|
-
/**
|
|
18
|
-
* Upload Buffer data to Tencent Cloud COS.
|
|
19
|
-
*
|
|
20
|
-
* Same COS upload implementation as uploadBufferToCos in media.ts.
|
|
21
|
-
* Initializes COS SDK with temp credentials from genUploadInfo, then calls putObject.
|
|
22
|
-
* Uses dynamic import for cos-nodejs-sdk-v5, compatible with both CJS and ESM.
|
|
23
|
-
*/
|
|
24
18
|
async function uploadBufferToCos(config, data) {
|
|
25
|
-
|
|
26
|
-
try {
|
|
27
|
-
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
28
|
-
COS = require("cos-nodejs-sdk-v5");
|
|
29
|
-
if (COS?.default) {
|
|
30
|
-
COS = COS.default;
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
catch {
|
|
34
|
-
try {
|
|
35
|
-
const pkg = await import("cos-nodejs-sdk-v5");
|
|
36
|
-
COS = pkg.default ?? pkg;
|
|
37
|
-
}
|
|
38
|
-
catch {
|
|
39
|
-
throw new Error("缺少依赖 cos-nodejs-sdk-v5,请运行 pnpm add cos-nodejs-sdk-v5");
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
const COSConstructor = COS;
|
|
43
|
-
const cos = new COSConstructor({
|
|
44
|
-
FileParallelLimit: 10,
|
|
45
|
-
getAuthorization(_, callback) {
|
|
46
|
-
callback({
|
|
47
|
-
TmpSecretId: config.encryptTmpSecretId,
|
|
48
|
-
TmpSecretKey: config.encryptTmpSecretKey,
|
|
49
|
-
SecurityToken: config.encryptToken,
|
|
50
|
-
StartTime: config.startTime,
|
|
51
|
-
ExpiredTime: config.expiredTime,
|
|
52
|
-
ScopeLimit: true,
|
|
53
|
-
});
|
|
54
|
-
},
|
|
55
|
-
UseAccelerate: true,
|
|
56
|
-
});
|
|
19
|
+
const cos = createCosClient(config);
|
|
57
20
|
await cos.putObject({
|
|
58
21
|
Bucket: config.bucketName,
|
|
59
22
|
Region: config.region,
|
|
@@ -17,7 +17,7 @@ export declare function getLatestStableVersion(): Promise<string | null>;
|
|
|
17
17
|
*/
|
|
18
18
|
export declare function isPublishedVersionOnNpm(version: string): Promise<boolean>;
|
|
19
19
|
/**
|
|
20
|
-
*
|
|
20
|
+
* Query `openclaw plugins inspect --json` and return the installed version of the specified plugin.
|
|
21
21
|
* Returns null if plugin is not installed or parsing fails.
|
|
22
22
|
*/
|
|
23
23
|
export declare function readInstalledVersion(pluginId: string): Promise<string | null>;
|
|
@@ -123,30 +123,33 @@ export async function isPublishedVersionOnNpm(version) {
|
|
|
123
123
|
}
|
|
124
124
|
}
|
|
125
125
|
/**
|
|
126
|
-
*
|
|
126
|
+
* Query `openclaw plugins inspect --json` and return the installed version of the specified plugin.
|
|
127
127
|
* Returns null if plugin is not installed or parsing fails.
|
|
128
128
|
*/
|
|
129
129
|
export async function readInstalledVersion(pluginId) {
|
|
130
130
|
log.info("读取已安装版本", { pluginId });
|
|
131
|
-
const result = await runOpenClawCommand(["plugins", "
|
|
131
|
+
const result = await runOpenClawCommand(["plugins", "inspect", pluginId, "--json"]);
|
|
132
132
|
if (!result.ok) {
|
|
133
|
-
log.warn("openclaw plugins
|
|
133
|
+
log.warn("openclaw plugins inspect 执行失败", {
|
|
134
134
|
summary: result.error,
|
|
135
135
|
...(result.stderr ? { stderr: result.stderr } : {}),
|
|
136
136
|
});
|
|
137
137
|
return null;
|
|
138
138
|
}
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
139
|
+
try {
|
|
140
|
+
const data = JSON.parse(result.stdout ?? "");
|
|
141
|
+
const version = data.plugin?.version;
|
|
142
|
+
if (version && isValidVersion(version)) {
|
|
143
|
+
log.info("已安装版本", { pluginId, version });
|
|
144
|
+
return version;
|
|
146
145
|
}
|
|
146
|
+
log.warn("未检测到已安装版本", { pluginId, raw: result.stdout });
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
catch (e) {
|
|
150
|
+
log.warn("解析 inspect JSON 失败", { pluginId, summary: firstLine(e) });
|
|
151
|
+
return null;
|
|
147
152
|
}
|
|
148
|
-
log.warn("未检测到已安装版本", { pluginId });
|
|
149
|
-
return null;
|
|
150
153
|
}
|
|
151
154
|
/**
|
|
152
155
|
* Snapshot `channels.yuanbao` config, outputting a JSON string usable with `config set ... --strict-json`.
|
|
@@ -26,10 +26,15 @@ export type MediaHistoryEntry = {
|
|
|
26
26
|
};
|
|
27
27
|
/** Group chat message history Map, keyed by groupCode */
|
|
28
28
|
export declare const chatHistories: Map<string, GroupHistoryEntry[]>;
|
|
29
|
-
/** Media history LRU, keyed by
|
|
29
|
+
/** Media history LRU, keyed by chatKey. Not cleared by clearHistoryEntriesIfEnabled. */
|
|
30
30
|
export declare const chatMediaHistories: Map<string, MediaHistoryEntry[]>;
|
|
31
|
+
/**
|
|
32
|
+
* Derive a chat-level cache key for media history.
|
|
33
|
+
* Format follows prepare-sender convention: `group:{groupCode}` / `direct:{fromAccount}`.
|
|
34
|
+
*/
|
|
35
|
+
export declare function deriveChatKey(isGroup: boolean, groupCode?: string, fromAccount?: string): string;
|
|
31
36
|
/**
|
|
32
37
|
* Write media entry to standalone LRU, evicting oldest entries when exceeding limit.
|
|
33
38
|
* Decoupled from text `chatHistories` to prevent media loss when text history is cleared after @bot.
|
|
34
39
|
*/
|
|
35
|
-
export declare function recordMediaHistory(
|
|
40
|
+
export declare function recordMediaHistory(chatKey: string, entry: MediaHistoryEntry): void;
|
|
@@ -8,24 +8,34 @@
|
|
|
8
8
|
*/
|
|
9
9
|
/** Group chat message history Map, keyed by groupCode */
|
|
10
10
|
export const chatHistories = new Map();
|
|
11
|
-
const
|
|
12
|
-
/** Media history LRU, keyed by
|
|
11
|
+
const MEDIA_HISTORY_MAX_PER_CHAT = 50;
|
|
12
|
+
/** Media history LRU, keyed by chatKey. Not cleared by clearHistoryEntriesIfEnabled. */
|
|
13
13
|
export const chatMediaHistories = new Map();
|
|
14
|
+
/**
|
|
15
|
+
* Derive a chat-level cache key for media history.
|
|
16
|
+
* Format follows prepare-sender convention: `group:{groupCode}` / `direct:{fromAccount}`.
|
|
17
|
+
*/
|
|
18
|
+
export function deriveChatKey(isGroup, groupCode, fromAccount) {
|
|
19
|
+
if (isGroup && groupCode) {
|
|
20
|
+
return `group:${groupCode}`;
|
|
21
|
+
}
|
|
22
|
+
return `direct:${fromAccount ?? "unknown"}`;
|
|
23
|
+
}
|
|
14
24
|
/**
|
|
15
25
|
* Write media entry to standalone LRU, evicting oldest entries when exceeding limit.
|
|
16
26
|
* Decoupled from text `chatHistories` to prevent media loss when text history is cleared after @bot.
|
|
17
27
|
*/
|
|
18
|
-
export function recordMediaHistory(
|
|
28
|
+
export function recordMediaHistory(chatKey, entry) {
|
|
19
29
|
if (entry.medias.length === 0) {
|
|
20
30
|
return;
|
|
21
31
|
}
|
|
22
|
-
let list = chatMediaHistories.get(
|
|
32
|
+
let list = chatMediaHistories.get(chatKey);
|
|
23
33
|
if (!list) {
|
|
24
34
|
list = [];
|
|
25
|
-
chatMediaHistories.set(
|
|
35
|
+
chatMediaHistories.set(chatKey, list);
|
|
26
36
|
}
|
|
27
37
|
list.push(entry);
|
|
28
|
-
if (list.length >
|
|
29
|
-
list.splice(0, list.length -
|
|
38
|
+
if (list.length > MEDIA_HISTORY_MAX_PER_CHAT) {
|
|
39
|
+
list.splice(0, list.length - MEDIA_HISTORY_MAX_PER_CHAT);
|
|
30
40
|
}
|
|
31
41
|
}
|
|
@@ -20,9 +20,10 @@ void test("extractTextFromMsgBody 处理混合消息体", () => {
|
|
|
20
20
|
{
|
|
21
21
|
msg_type: "TIMImageElem",
|
|
22
22
|
msg_content: {
|
|
23
|
+
uuid: "shot.jpeg",
|
|
23
24
|
image_info_array: [
|
|
24
|
-
{ type: 1, url: "https://example.com/img.png" },
|
|
25
|
-
{ type: 2, url: "https://example.com/medium.png" },
|
|
25
|
+
{ type: 1, width: 800, height: 600, url: "https://example.com/img.png" },
|
|
26
|
+
{ type: 2, width: 400, height: 300, url: "https://example.com/medium.png" },
|
|
26
27
|
],
|
|
27
28
|
},
|
|
28
29
|
},
|
|
@@ -30,9 +31,10 @@ void test("extractTextFromMsgBody 处理混合消息体", () => {
|
|
|
30
31
|
]);
|
|
31
32
|
assert.ok(result.rawBody.includes("hello"));
|
|
32
33
|
assert.ok(result.rawBody.includes("world"));
|
|
33
|
-
assert.ok(result.rawBody.includes("[
|
|
34
|
+
assert.ok(result.rawBody.includes("[image:shot_400_300.jpeg]"), `rawBody was: ${result.rawBody}`);
|
|
34
35
|
assert.equal(result.medias.length, 1);
|
|
35
36
|
assert.equal(result.medias[0].url, "https://example.com/medium.png");
|
|
37
|
+
assert.equal(result.medias[0].mediaName, "shot_400_300.jpeg");
|
|
36
38
|
});
|
|
37
39
|
void test("extractTextFromMsgBody 处理 @Bot 消息", () => {
|
|
38
40
|
const ctx = makeMockCtx("bot-001");
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* File message: on input, extracts file URL and filename to media list and returns file identifier;
|
|
5
5
|
* on output, constructs file message body.
|
|
6
6
|
*/
|
|
7
|
+
import { sanitizeMediaFilename } from "../../utils/media.js";
|
|
7
8
|
export const fileHandler = {
|
|
8
9
|
msgType: "TIMFileElem",
|
|
9
10
|
/**
|
|
@@ -14,10 +15,10 @@ export const fileHandler = {
|
|
|
14
15
|
const fileUrl = elem.msg_content?.url;
|
|
15
16
|
const fileName = elem.msg_content?.file_name;
|
|
16
17
|
if (fileUrl) {
|
|
17
|
-
resData.medias.
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
const fileCount = resData.medias.filter(m => m.mediaType === "file").length + 1;
|
|
19
|
+
const displayName = sanitizeMediaFilename(fileName, `file${fileCount}`);
|
|
20
|
+
resData.medias.push({ mediaType: "file", url: fileUrl, mediaName: displayName });
|
|
21
|
+
return `[file:${displayName}]`;
|
|
21
22
|
}
|
|
22
23
|
return "[file]";
|
|
23
24
|
},
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TIMImageElem message handler.
|
|
3
3
|
*
|
|
4
|
-
* Image message: on input, extracts image URL to media list and returns
|
|
5
|
-
* on output, constructs image message body.
|
|
4
|
+
* Image message: on input, extracts image URL to media list and returns
|
|
5
|
+
* [image:{name}_{w}_{h}.{ext}] placeholder; on output, constructs image message body.
|
|
6
6
|
*/
|
|
7
7
|
import type { MessageElemHandler } from "./types.js";
|
|
8
8
|
export declare const imageHandler: MessageElemHandler;
|
|
@@ -1,24 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* TIMImageElem message handler.
|
|
3
3
|
*
|
|
4
|
-
* Image message: on input, extracts image URL to media list and returns
|
|
5
|
-
* on output, constructs image message body.
|
|
4
|
+
* Image message: on input, extracts image URL to media list and returns
|
|
5
|
+
* [image:{name}_{w}_{h}.{ext}] placeholder; on output, constructs image message body.
|
|
6
6
|
*/
|
|
7
|
+
import { sanitizeMediaFilename } from "../../utils/media.js";
|
|
8
|
+
/**
|
|
9
|
+
* Build a descriptive filename for an image from its uuid and dimensions.
|
|
10
|
+
* e.g. uuid="c9a1b3f8784898.jpeg", w=720, h=1793 → "c9a1b3f8784898_720_1793.jpeg"
|
|
11
|
+
* Falls back to plain uuid (no dimensions available) or "image{N}" (no uuid).
|
|
12
|
+
*/
|
|
13
|
+
function buildImageMediaName(uuid, w, h, fallbackIndex) {
|
|
14
|
+
const fallback = `image${fallbackIndex}`;
|
|
15
|
+
if (!uuid) {
|
|
16
|
+
return fallback;
|
|
17
|
+
}
|
|
18
|
+
const dotIdx = uuid.lastIndexOf(".");
|
|
19
|
+
const stem = dotIdx > 0 ? uuid.slice(0, dotIdx) : uuid;
|
|
20
|
+
const ext = dotIdx > 0 ? uuid.slice(dotIdx) : "";
|
|
21
|
+
const raw = w && h ? `${stem}_${w}_${h}${ext}` : uuid;
|
|
22
|
+
return sanitizeMediaFilename(raw, fallback);
|
|
23
|
+
}
|
|
7
24
|
export const imageHandler = {
|
|
8
25
|
msgType: "TIMImageElem",
|
|
9
26
|
/**
|
|
10
27
|
* Extract image URL and record to media list.
|
|
11
|
-
* Returns [
|
|
28
|
+
* Returns [image:{mediaName}] placeholder; undefined if no URL.
|
|
29
|
+
* mediaName encodes uuid + selected resolution (width x height).
|
|
12
30
|
*/
|
|
13
31
|
extract(_ctx, elem, resData) {
|
|
14
|
-
// Get url from first element in image_info_array (original image, type=1)
|
|
15
32
|
const imageInfoArray = elem.msg_content?.image_info_array;
|
|
16
|
-
//
|
|
33
|
+
// Prefer medium-size image (index 1) for download; fall back to original (index 0)
|
|
17
34
|
const imageInfo = imageInfoArray?.[1] || imageInfoArray?.[0];
|
|
18
35
|
if (imageInfo?.url) {
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
36
|
+
const uuid = elem.msg_content?.uuid ?? "";
|
|
37
|
+
const imageCount = resData.medias.filter(m => m.mediaType === "image").length;
|
|
38
|
+
const mediaName = buildImageMediaName(uuid, imageInfo.width, imageInfo.height, imageCount + 1);
|
|
39
|
+
resData.medias.push({ mediaType: "image", url: imageInfo.url, mediaName });
|
|
40
|
+
return `[image:${mediaName}]`;
|
|
22
41
|
}
|
|
23
42
|
return undefined;
|
|
24
43
|
},
|
|
@@ -24,18 +24,51 @@ void test("imageHandler extract extracts image URL to media list", () => {
|
|
|
24
24
|
const elem = {
|
|
25
25
|
msg_type: "TIMImageElem",
|
|
26
26
|
msg_content: {
|
|
27
|
+
uuid: "abc123.jpeg",
|
|
27
28
|
image_info_array: [
|
|
28
|
-
{ type: 1, url: "https://example.com/original.png" },
|
|
29
|
-
{ type: 2, url: "https://example.com/medium.png" },
|
|
29
|
+
{ type: 1, width: 735, height: 1830, url: "https://example.com/original.png" },
|
|
30
|
+
{ type: 2, width: 720, height: 1793, url: "https://example.com/medium.png" },
|
|
30
31
|
],
|
|
31
32
|
},
|
|
32
33
|
};
|
|
33
34
|
const result = imageHandler.extract(ctx, elem, resData);
|
|
34
|
-
assert.equal(result, "[
|
|
35
|
+
assert.equal(result, "[image:abc123_720_1793.jpeg]");
|
|
35
36
|
assert.equal(resData.medias.length, 1);
|
|
36
37
|
// Prefers medium image (index 1)
|
|
37
38
|
assert.equal(resData.medias[0].url, "https://example.com/medium.png");
|
|
38
39
|
assert.equal(resData.medias[0].mediaType, "image");
|
|
40
|
+
assert.equal(resData.medias[0].mediaName, "abc123_720_1793.jpeg");
|
|
41
|
+
});
|
|
42
|
+
void test("imageHandler extract falls back to plain uuid when no dimensions", () => {
|
|
43
|
+
const ctx = makeMockCtx();
|
|
44
|
+
const resData = makeResData();
|
|
45
|
+
const elem = {
|
|
46
|
+
msg_type: "TIMImageElem",
|
|
47
|
+
msg_content: {
|
|
48
|
+
uuid: "photo.jpg",
|
|
49
|
+
image_info_array: [
|
|
50
|
+
{ type: 1, url: "https://example.com/photo.jpg" },
|
|
51
|
+
],
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
const result = imageHandler.extract(ctx, elem, resData);
|
|
55
|
+
assert.equal(result, "[image:photo.jpg]");
|
|
56
|
+
assert.equal(resData.medias[0].mediaName, "photo.jpg");
|
|
57
|
+
});
|
|
58
|
+
void test("imageHandler extract falls back to image{N} when no uuid", () => {
|
|
59
|
+
const ctx = makeMockCtx();
|
|
60
|
+
const resData = makeResData();
|
|
61
|
+
const elem = {
|
|
62
|
+
msg_type: "TIMImageElem",
|
|
63
|
+
msg_content: {
|
|
64
|
+
image_info_array: [
|
|
65
|
+
{ type: 1, width: 100, height: 200, url: "https://example.com/img.png" },
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
const result = imageHandler.extract(ctx, elem, resData);
|
|
70
|
+
assert.equal(result, "[image:image1]");
|
|
71
|
+
assert.equal(resData.medias[0].mediaName, "image1");
|
|
39
72
|
});
|
|
40
73
|
void test("imageHandler extract returns undefined when no URL", () => {
|
|
41
74
|
const ctx = makeMockCtx();
|
|
@@ -1,22 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Quote message parsing
|
|
3
|
-
*
|
|
4
|
-
* Extracts quoted messages from cloud_custom_data and formats them as context text.
|
|
2
|
+
* Quote message business logic: parsing, media desc resolution, and formatting.
|
|
5
3
|
*/
|
|
6
4
|
import type { QuoteInfo } from "../../types.js";
|
|
7
5
|
/**
|
|
8
|
-
* Parse quote info from cloud_custom_data JSON string.
|
|
9
|
-
*
|
|
6
|
+
* Parse quote info from cloud_custom_data JSON string and resolve its desc.
|
|
7
|
+
*
|
|
8
|
+
* For media-type quotes (image/file/video/voice) whose desc is empty,
|
|
9
|
+
* resolves actual filenames from the media history LRU. Falls back to a
|
|
10
|
+
* generic label (e.g. "[image]") when LRU data is unavailable.
|
|
11
|
+
*
|
|
12
|
+
* @param chatKey — used for LRU lookup; pass deriveChatKey() result.
|
|
13
|
+
* @returns undefined if no quote exists, parsing fails, or the quote carries
|
|
14
|
+
* no useful information (empty desc AND not a recognized media type).
|
|
15
|
+
*/
|
|
16
|
+
export declare function parseQuoteFromCloudCustomData(cloudCustomData: string | undefined, chatKey?: string): QuoteInfo | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a descriptive desc for a media-type quote.
|
|
19
|
+
* Looks up actual filenames from the media history LRU; falls back to
|
|
20
|
+
* a generic label (e.g. "[image]") when LRU data is unavailable.
|
|
10
21
|
*/
|
|
11
|
-
export declare function
|
|
22
|
+
export declare function resolveMediaQuoteDesc(type: number, quoteId: string | undefined, chatKey: string | undefined): string;
|
|
12
23
|
/**
|
|
13
|
-
* Format quoted message into context text
|
|
24
|
+
* Format quoted message into context text for AI consumption.
|
|
14
25
|
*
|
|
15
|
-
* Generated format:
|
|
16
26
|
* ```
|
|
17
|
-
* [Quoted message from <sender_nickname>]:
|
|
18
|
-
*
|
|
19
|
-
* ---
|
|
27
|
+
* > [Quoted message from <sender_nickname>]:
|
|
28
|
+
* ><desc>
|
|
20
29
|
* ```
|
|
21
30
|
*/
|
|
22
31
|
export declare function formatQuoteContext(quote: QuoteInfo): string;
|