openclaw-plugin-yuanbao 2.13.5 → 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/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/pipeline/middlewares/build-context.js +2 -2
- 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/test-helpers/mock-ctx.js +1 -0
- package/dist/src/business/pipeline/types.d.ts +2 -0
- package/dist/src/dispatcher/debouncer/index.js +14 -1
- package/dist/src/types.d.ts +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
};
|
|
@@ -8,7 +8,7 @@ import { YUANBAO_MARKDOWN_HINT } from "../../messaging/context.js";
|
|
|
8
8
|
export const buildContext = {
|
|
9
9
|
name: "build-context",
|
|
10
10
|
handler: async (ctx, next) => {
|
|
11
|
-
const { core, account, isGroup, fromAccount, senderNickname, groupCode, rewrittenBody, mediaPaths, mediaTypes, commandAuthorized, route, storePath, envelopeOptions, previousTimestamp, raw, } = ctx;
|
|
11
|
+
const { core, account, isGroup, fromAccount, senderNickname, groupCode, rewrittenBody, commandParts, mediaPaths, mediaTypes, commandAuthorized, route, storePath, envelopeOptions, previousTimestamp, raw, } = ctx;
|
|
12
12
|
if (!route || !storePath || !envelopeOptions) {
|
|
13
13
|
ctx.log.error("[build-context] prerequisite middleware not ready");
|
|
14
14
|
return;
|
|
@@ -55,7 +55,7 @@ export const buildContext = {
|
|
|
55
55
|
BodyForAgent: rewrittenBody,
|
|
56
56
|
...(isGroup ? { InboundHistory: inboundHistory } : {}),
|
|
57
57
|
RawBody: rewrittenBody,
|
|
58
|
-
CommandBody: rewrittenBody,
|
|
58
|
+
CommandBody: commandParts?.length > 0 ? commandParts.join(" ") : rewrittenBody,
|
|
59
59
|
From: `yuanbao:${label}`,
|
|
60
60
|
To: `yuanbao:${label}`,
|
|
61
61
|
SessionKey: route.sessionKey,
|
|
@@ -3,16 +3,34 @@
|
|
|
3
3
|
* Checks for control commands and applies DM allowFrom + useAccessGroups policies.
|
|
4
4
|
*/
|
|
5
5
|
import { resolveControlCommandGate } from "openclaw/plugin-sdk/command-auth";
|
|
6
|
+
/** Extract pure text content from msg_body (TIMTextElem only, skipping @mention custom elements). */
|
|
7
|
+
function extractTextOnly(ctx) {
|
|
8
|
+
if (!ctx.raw.msg_body)
|
|
9
|
+
return ctx.rawBody;
|
|
10
|
+
return ctx.raw.msg_body
|
|
11
|
+
.filter(e => e.msg_type === "TIMTextElem")
|
|
12
|
+
.map(e => e.msg_content?.text ?? "")
|
|
13
|
+
.join("")
|
|
14
|
+
.trim() || ctx.rawBody;
|
|
15
|
+
}
|
|
6
16
|
export const guardCommand = {
|
|
7
17
|
name: "guard-command",
|
|
8
18
|
handler: async (ctx, next) => {
|
|
9
19
|
const { core, config, rawBody, fromAccount, account } = ctx;
|
|
20
|
+
// Group chat: extract TIMTextElem-only text for command detection
|
|
21
|
+
// (rawBody includes @mention custom elements which break command matching).
|
|
22
|
+
const commandText = ctx.isGroup ? extractTextOnly(ctx) : rawBody;
|
|
10
23
|
const allowTextCommands = core.channel.commands.shouldHandleTextCommands({
|
|
11
24
|
cfg: config,
|
|
12
25
|
surface: "yuanbao",
|
|
13
26
|
});
|
|
14
|
-
const
|
|
27
|
+
const rawHasControlCommand = core.channel.text.hasControlCommand(commandText, config);
|
|
28
|
+
const hasControlCommand = ctx.isGroup ? rawHasControlCommand && ctx.isAtBot : rawHasControlCommand;
|
|
15
29
|
ctx.hasControlCommand = hasControlCommand;
|
|
30
|
+
if (!hasControlCommand) {
|
|
31
|
+
await next();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
16
34
|
// Build DM policy allowFrom
|
|
17
35
|
const dmPolicy = account.config.dm?.policy ?? "open";
|
|
18
36
|
const rawAllowFrom = (account.config.dm?.allowFrom ?? []).map(String);
|
|
@@ -28,8 +46,9 @@ export const guardCommand = {
|
|
|
28
46
|
ctx.commandAuthorized = commandAuthorized;
|
|
29
47
|
if (shouldBlock) {
|
|
30
48
|
ctx.log.info(`[guard-command] control command unauthorized, discarding <- ${ctx.isGroup ? `group:${ctx.groupCode}` : ""} from: ${fromAccount}`);
|
|
31
|
-
return;
|
|
49
|
+
return;
|
|
32
50
|
}
|
|
51
|
+
ctx.commandParts = commandText.trim().split(/\s+/);
|
|
33
52
|
await next();
|
|
34
53
|
},
|
|
35
54
|
};
|
|
@@ -7,33 +7,36 @@
|
|
|
7
7
|
import { getMember } from "../../../infra/cache/member.js";
|
|
8
8
|
import { sendGroupMsgBody } from "../../../infra/transport.js";
|
|
9
9
|
import { prepareOutboundContent, buildOutboundMsgBody } from "../../messaging/handlers/index.js";
|
|
10
|
-
/**
|
|
11
|
-
const
|
|
10
|
+
/** Commands allowed in group chat (owner-only) */
|
|
11
|
+
const GROUP_ALLOWED_COMMANDS = new Set([
|
|
12
|
+
"/new", "/reset", "/retry", "/undo", "/stop",
|
|
13
|
+
"/approve", "/btw", "/queue",
|
|
14
|
+
]);
|
|
12
15
|
export const guardGroupCommand = {
|
|
13
16
|
name: "guard-group-command",
|
|
14
|
-
when: ctx => ctx.isGroup,
|
|
17
|
+
when: ctx => ctx.isGroup && ctx.hasControlCommand,
|
|
15
18
|
handler: async (ctx, next) => {
|
|
16
|
-
const {
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
|
|
23
|
-
if (
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
19
|
+
const { commandParts, raw, account, groupCode, fromAccount } = ctx;
|
|
20
|
+
const cmd = commandParts?.[0]?.toLowerCase() ?? "";
|
|
21
|
+
const ownerId = account.botOwnerId || raw.bot_owner_id;
|
|
22
|
+
const isOwner = Boolean(ownerId && raw.from_account === ownerId);
|
|
23
|
+
ctx.log.info('[guard-group-command] come in', { isOwner, cmd });
|
|
24
|
+
const allowed = GROUP_ALLOWED_COMMANDS.has(cmd);
|
|
25
|
+
const rejected = !allowed || !isOwner;
|
|
26
|
+
if (rejected) {
|
|
27
|
+
const rejectReason = !allowed
|
|
28
|
+
? `⚠️ ${cmd} 暂不支持在群聊中使用,请在私聊中发送`
|
|
29
|
+
: `⚠️ ${cmd} 仅限创建者使用哦~`;
|
|
30
|
+
await sendGroupMsgBody({
|
|
31
|
+
account,
|
|
32
|
+
groupCode: groupCode,
|
|
33
|
+
msgBody: buildOutboundMsgBody(prepareOutboundContent(rejectReason, groupCode, getMember(account.accountId))),
|
|
34
|
+
fromAccount: account.botId,
|
|
35
|
+
refMsgId: raw.msg_id || raw.msg_key || undefined,
|
|
36
|
+
refFromAccount: fromAccount,
|
|
37
|
+
wsClient: ctx.wsClient,
|
|
38
|
+
});
|
|
39
|
+
return;
|
|
37
40
|
}
|
|
38
41
|
await next();
|
|
39
42
|
},
|
|
@@ -28,95 +28,72 @@ function setupMocks(t) {
|
|
|
28
28
|
},
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
|
-
void test("guard-group-command: when guard - executes
|
|
31
|
+
void test("guard-group-command: when guard - executes for group command", async (t) => {
|
|
32
32
|
setupMocks(t);
|
|
33
33
|
const { guardGroupCommand } = await import("./guard-group-command.js");
|
|
34
|
-
const ctx = createMockCtx({ isGroup: true });
|
|
34
|
+
const ctx = createMockCtx({ isGroup: true, hasControlCommand: true });
|
|
35
35
|
assert.equal(guardGroupCommand.when(ctx), true);
|
|
36
36
|
});
|
|
37
37
|
void test("guard-group-command: when guard - skips in C2C", async (t) => {
|
|
38
38
|
setupMocks(t);
|
|
39
39
|
const { guardGroupCommand } = await import("./guard-group-command.js");
|
|
40
|
-
const ctx = createMockCtx({ isGroup: false });
|
|
40
|
+
const ctx = createMockCtx({ isGroup: false, hasControlCommand: true });
|
|
41
41
|
assert.equal(guardGroupCommand.when(ctx), false);
|
|
42
42
|
});
|
|
43
|
-
void test("guard-group-command:
|
|
43
|
+
void test("guard-group-command: when guard - skips non-command in group", async (t) => {
|
|
44
|
+
setupMocks(t);
|
|
45
|
+
const { guardGroupCommand } = await import("./guard-group-command.js");
|
|
46
|
+
const ctx = createMockCtx({ isGroup: true, hasControlCommand: false });
|
|
47
|
+
assert.equal(guardGroupCommand.when(ctx), false);
|
|
48
|
+
});
|
|
49
|
+
void test("guard-group-command: non-owner + whitelisted command -> reject (owner-only)", async (t) => {
|
|
44
50
|
setupMocks(t);
|
|
45
51
|
const { guardGroupCommand } = await import("./guard-group-command.js");
|
|
46
52
|
const ctx = createMockCtx({
|
|
47
53
|
isGroup: true,
|
|
48
|
-
|
|
54
|
+
hasControlCommand: true,
|
|
55
|
+
commandParts: ["/new"],
|
|
56
|
+
rawBody: "/new",
|
|
49
57
|
groupCode: "group-001",
|
|
50
58
|
fromAccount: "user-001",
|
|
51
59
|
raw: { bot_owner_id: "owner-001", from_account: "user-001", msg_id: "msg-001" },
|
|
52
|
-
core: {
|
|
53
|
-
channel: {
|
|
54
|
-
text: { hasControlCommand: () => true },
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
60
|
config: {},
|
|
58
61
|
});
|
|
59
62
|
const { next, wasCalled } = createMockNext();
|
|
60
63
|
await guardGroupCommand.handler(ctx, next);
|
|
61
64
|
assert.equal(wasCalled(), false, "non-owner should abort pipeline");
|
|
62
65
|
});
|
|
63
|
-
void test("guard-group-command: owner
|
|
66
|
+
void test("guard-group-command: owner + whitelisted command -> pass through", async (t) => {
|
|
64
67
|
setupMocks(t);
|
|
65
68
|
const { guardGroupCommand } = await import("./guard-group-command.js");
|
|
66
69
|
const ctx = createMockCtx({
|
|
67
70
|
isGroup: true,
|
|
68
|
-
|
|
71
|
+
hasControlCommand: true,
|
|
72
|
+
commandParts: ["/new"],
|
|
73
|
+
rawBody: "/new",
|
|
69
74
|
groupCode: "group-001",
|
|
70
75
|
fromAccount: "owner-001",
|
|
71
76
|
raw: { bot_owner_id: "owner-001", from_account: "owner-001" },
|
|
72
|
-
core: {
|
|
73
|
-
channel: {
|
|
74
|
-
text: { hasControlCommand: () => true },
|
|
75
|
-
},
|
|
76
|
-
},
|
|
77
77
|
config: {},
|
|
78
78
|
});
|
|
79
79
|
const { next, wasCalled } = createMockNext();
|
|
80
80
|
await guardGroupCommand.handler(ctx, next);
|
|
81
81
|
assert.equal(wasCalled(), true, "owner should pass through");
|
|
82
82
|
});
|
|
83
|
-
void test("guard-group-command:
|
|
84
|
-
setupMocks(t);
|
|
85
|
-
const { guardGroupCommand } = await import("./guard-group-command.js");
|
|
86
|
-
const ctx = createMockCtx({
|
|
87
|
-
isGroup: true,
|
|
88
|
-
rawBody: "/random-text",
|
|
89
|
-
groupCode: "group-001",
|
|
90
|
-
fromAccount: "user-001",
|
|
91
|
-
raw: { bot_owner_id: "owner-001", from_account: "user-001" },
|
|
92
|
-
core: {
|
|
93
|
-
channel: {
|
|
94
|
-
text: { hasControlCommand: () => false },
|
|
95
|
-
},
|
|
96
|
-
},
|
|
97
|
-
config: {},
|
|
98
|
-
});
|
|
99
|
-
const { next, wasCalled } = createMockNext();
|
|
100
|
-
await guardGroupCommand.handler(ctx, next);
|
|
101
|
-
assert.equal(wasCalled(), true, "unregistered command should pass through");
|
|
102
|
-
});
|
|
103
|
-
void test("guard-group-command: plain text message -> pass through", async (t) => {
|
|
83
|
+
void test("guard-group-command: non-whitelisted command -> reject", async (t) => {
|
|
104
84
|
setupMocks(t);
|
|
105
85
|
const { guardGroupCommand } = await import("./guard-group-command.js");
|
|
106
86
|
const ctx = createMockCtx({
|
|
107
87
|
isGroup: true,
|
|
108
|
-
|
|
88
|
+
hasControlCommand: true,
|
|
89
|
+
commandParts: ["/config"],
|
|
90
|
+
rawBody: "/config",
|
|
109
91
|
groupCode: "group-001",
|
|
110
92
|
fromAccount: "user-001",
|
|
111
|
-
raw: { from_account: "user-001" },
|
|
112
|
-
core: {
|
|
113
|
-
channel: {
|
|
114
|
-
text: { hasControlCommand: () => false },
|
|
115
|
-
},
|
|
116
|
-
},
|
|
93
|
+
raw: { bot_owner_id: "owner-001", from_account: "user-001", msg_id: "msg-001" },
|
|
117
94
|
config: {},
|
|
118
95
|
});
|
|
119
96
|
const { next, wasCalled } = createMockNext();
|
|
120
97
|
await guardGroupCommand.handler(ctx, next);
|
|
121
|
-
assert.equal(wasCalled(),
|
|
98
|
+
assert.equal(wasCalled(), false, "non-whitelisted command should abort pipeline");
|
|
122
99
|
});
|
|
@@ -6,9 +6,11 @@ import { parseUpgradeCommand } from "../../commands/upgrade/index.js";
|
|
|
6
6
|
import { performUpgrade } from "../../commands/upgrade/upgrade.js";
|
|
7
7
|
/**
|
|
8
8
|
* Check whether the message is from the bot owner.
|
|
9
|
+
* Prefers account.botOwnerId (cached via QueryBotInfoReq) over per-message bot_owner_id.
|
|
9
10
|
*/
|
|
10
|
-
function isOwnerMessage(raw) {
|
|
11
|
-
|
|
11
|
+
function isOwnerMessage(raw, accountOwnerId) {
|
|
12
|
+
const ownerId = accountOwnerId || raw.bot_owner_id;
|
|
13
|
+
return Boolean(ownerId && raw.from_account === ownerId);
|
|
12
14
|
}
|
|
13
15
|
/**
|
|
14
16
|
* Send a reply message via sendText action + deliver layer.
|
|
@@ -34,7 +36,7 @@ export const guardSpecialCommand = {
|
|
|
34
36
|
const upgradeCmd = parseUpgradeCommand(trimmedBody);
|
|
35
37
|
if (upgradeCmd.matched) {
|
|
36
38
|
ctx.log.info(`[guard-special-command] received ${trimmedBody} command`);
|
|
37
|
-
if (!isOwnerMessage(raw)) {
|
|
39
|
+
if (!isOwnerMessage(raw, ctx.account.botOwnerId)) {
|
|
38
40
|
ctx.log.warn(`[guard-special-command] non-owner attempted ${trimmedBody}, rejected`, {
|
|
39
41
|
fromAccount,
|
|
40
42
|
});
|
|
@@ -75,7 +77,7 @@ export const guardSpecialCommand = {
|
|
|
75
77
|
// /issue-log owner guard
|
|
76
78
|
if (trimmedBody.startsWith("/issue-log")) {
|
|
77
79
|
ctx.log.info("[guard-special-command] received /issue-log command");
|
|
78
|
-
if (!isOwnerMessage(raw)) {
|
|
80
|
+
if (!isOwnerMessage(raw, ctx.account.botOwnerId)) {
|
|
79
81
|
ctx.log.warn("[guard-special-command] non-owner attempted /issue-log, rejected", {
|
|
80
82
|
fromAccount,
|
|
81
83
|
});
|
|
@@ -59,6 +59,8 @@ export interface PipelineContext {
|
|
|
59
59
|
commandAuthorized: boolean;
|
|
60
60
|
rewrittenBody: string;
|
|
61
61
|
hasControlCommand: boolean;
|
|
62
|
+
/** Parsed command parts: [command, ...args], e.g. ["/new"] or ["/reset", "arg1"]. Empty when not a command. */
|
|
63
|
+
commandParts: string[];
|
|
62
64
|
effectiveWasMentioned: boolean;
|
|
63
65
|
mediaPaths: string[];
|
|
64
66
|
mediaTypes: string[];
|
|
@@ -32,10 +32,17 @@ function extractRawText(item) {
|
|
|
32
32
|
.join("")
|
|
33
33
|
.trim();
|
|
34
34
|
}
|
|
35
|
+
function isOwnerMessage(item) {
|
|
36
|
+
const ownerId = item.account.botOwnerId || item.msg.bot_owner_id;
|
|
37
|
+
return Boolean(ownerId && item.msg.from_account === ownerId);
|
|
38
|
+
}
|
|
35
39
|
function buildSessionKey(item) {
|
|
36
40
|
const base = buildBaseSessionKey(item);
|
|
37
|
-
// Group chat: single queue
|
|
38
41
|
if (item.isGroup) {
|
|
42
|
+
const rawText = extractRawText(item);
|
|
43
|
+
if (isAbortRequestText(rawText) && isOwnerMessage(item)) {
|
|
44
|
+
return `${base}:control`;
|
|
45
|
+
}
|
|
39
46
|
return base;
|
|
40
47
|
}
|
|
41
48
|
const rawText = extractRawText(item);
|
|
@@ -118,6 +125,7 @@ function buildPipelineContext(primary, items) {
|
|
|
118
125
|
commandAuthorized: false,
|
|
119
126
|
rewrittenBody: "",
|
|
120
127
|
hasControlCommand: false,
|
|
128
|
+
commandParts: [],
|
|
121
129
|
effectiveWasMentioned: false,
|
|
122
130
|
mediaPaths: [],
|
|
123
131
|
mediaTypes: [],
|
|
@@ -167,6 +175,11 @@ export function ensureDebouncer(config) {
|
|
|
167
175
|
return;
|
|
168
176
|
}
|
|
169
177
|
const sessionKey = buildSessionKey(primary);
|
|
178
|
+
// Group owner /stop: invalidate base queue → skip remaining queued tasks
|
|
179
|
+
if (primary.isGroup && isAbortRequestText(extractRawText(primary)) && isOwnerMessage(primary)) {
|
|
180
|
+
const baseKey = buildBaseSessionKey(primary);
|
|
181
|
+
sessionQueue.invalidate(baseKey);
|
|
182
|
+
}
|
|
170
183
|
// ⭐ Direct normal message: abort old inference + invalidate queued old tasks
|
|
171
184
|
if (isDirectNormalMessage(primary)) {
|
|
172
185
|
const baseKey = buildBaseSessionKey(primary);
|
package/dist/src/types.d.ts
CHANGED
|
@@ -51,6 +51,8 @@ export type ResolvedYuanbaoAccount = {
|
|
|
51
51
|
appKey?: string;
|
|
52
52
|
appSecret?: string;
|
|
53
53
|
botId?: string;
|
|
54
|
+
/** Cached after WS connect via QueryBotInfoReq; takes priority over per-message bot_owner_id */
|
|
55
|
+
botOwnerId?: string;
|
|
54
56
|
apiDomain?: string;
|
|
55
57
|
wsUrl?: string;
|
|
56
58
|
token?: string;
|
package/openclaw.plugin.json
CHANGED