openclaw-plugin-yuanbao 2.0.1 → 2.1.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.
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-plugin-yuanbao",
3
3
  "name": "元宝 Bot",
4
4
  "description": "Tencent YuanBao intelligent bot channel plugin",
5
- "version": "2.0.1",
5
+ "version": "2.1.0",
6
6
  "channels": [
7
7
  "yuanbao"
8
8
  ],
@@ -90,7 +90,8 @@ export function resolveYuanbaoAccount(params) {
90
90
  ? merged.historyLimit
91
91
  : 100;
92
92
  const disableBlockStreaming = merged.disableBlockStreaming !== undefined ? merged.disableBlockStreaming : false;
93
- const fallbackReply = merged.fallbackReply?.trim() || undefined;
93
+ const requireMention = merged.requireMention !== undefined ? merged.requireMention : true;
94
+ const fallbackReply = merged.fallbackReply?.trim() || '暂时无法解答,你可以换个问题问问我哦';
94
95
  const configured = Boolean(appKey && appSecret);
95
96
  if (!configured && Boolean(yuanbaoConfig)) {
96
97
  warnIncompleteConfig(appKey, appSecret);
@@ -113,6 +114,7 @@ export function resolveYuanbaoAccount(params) {
113
114
  mediaMaxMb,
114
115
  historyLimit,
115
116
  disableBlockStreaming,
117
+ requireMention,
116
118
  fallbackReply,
117
119
  config: merged,
118
120
  };
@@ -59,10 +59,17 @@ export const yuanbaoConfigSchema = {
59
59
  description: '开启后将关闭分块流式发送能力,改为非分块输出',
60
60
  default: false,
61
61
  },
62
+ requireMention: {
63
+ type: 'boolean',
64
+ title: '群聊需要 @ 机器人',
65
+ description: '开启后群聊消息必须 @ 机器人才会触发回复;关闭后机器人回复所有群消息',
66
+ default: true,
67
+ },
62
68
  fallbackReply: {
63
69
  type: 'string',
64
70
  title: '兜底回复文案',
65
71
  description: '当 AI 未返回有效回复内容时,自动发送给用户的兜底文本',
72
+ default: '暂时无法解答,你可以换个问题问问我哦',
66
73
  },
67
74
  },
68
75
  additionalProperties: false,
@@ -1,4 +1,4 @@
1
- import type { HistoryEntry } from 'openclaw/plugin-sdk/reply-history';
1
+ import type { HistoryEntry } from 'openclaw/plugin-sdk/mattermost';
2
2
  export type GroupHistoryEntry = HistoryEntry & {
3
3
  medias?: Array<{
4
4
  url: string;
@@ -1,4 +1,4 @@
1
- import { recordPendingHistoryEntryIfEnabled, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, } from 'openclaw/plugin-sdk/reply-history';
1
+ import { recordPendingHistoryEntryIfEnabled, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, } from 'openclaw/plugin-sdk/mattermost';
2
2
  import { downloadMediasToLocalFiles, downloadAndUploadMedia, guessMimeType, buildImageMsgBody, buildFileMsgBody } from '../media.js';
3
3
  import { resolveOutboundSenderAccount, rewriteSlashCommand, YUANBAO_FINAL_TEXT_CHUNK_LIMIT, } from './context.js';
4
4
  import { extractTextFromMsgBody } from './extract.js';
@@ -329,7 +329,8 @@ async function handleGroupMessage(params) {
329
329
  }
330
330
  glog.debug(`开始处理群消息, 账号: ${account.accountId}, group: ${groupCode}`);
331
331
  const { historyLimit } = account;
332
- if (!isAtBot) {
332
+ const requireMention = account.requireMention !== false;
333
+ if (requireMention && !isAtBot) {
333
334
  glog.info(`非@机器人消息,已记录到群历史上下文,跳过回复 <- group:${groupCode}, from: ${fromAccount}`);
334
335
  if (historyLimit > 0) {
335
336
  recordPendingHistoryEntryIfEnabled({
@@ -484,6 +485,7 @@ async function handleGroupMessage(params) {
484
485
  msgBody: contentMsgBody,
485
486
  fromAccount: outboundSender,
486
487
  refMsgId,
488
+ refFromAccount: fromAccount,
487
489
  ctx,
488
490
  });
489
491
  },
@@ -495,6 +497,7 @@ async function handleGroupMessage(params) {
495
497
  msgBody: contentMsgBody,
496
498
  fromAccount: outboundSender,
497
499
  refMsgId,
500
+ refFromAccount: fromAccount,
498
501
  ctx,
499
502
  });
500
503
  },
@@ -517,14 +520,31 @@ async function handleGroupMessage(params) {
517
520
  const msgBody = mime.startsWith('image/')
518
521
  ? buildImageMsgBody({ url: uploadResult.url, filename: uploadResult.filename, size: uploadResult.size, uuid: uploadResult.uuid, imageInfo: uploadResult.imageInfo })
519
522
  : buildFileMsgBody({ url: uploadResult.url, filename: uploadResult.filename, size: uploadResult.size });
520
- const result = await sendMsgBodyDirect({ account, config, target: `group:${groupCode}`, msgBody: msgBody, wsClient: ctx.wsClient, core, refMsgId });
523
+ const result = await sendMsgBodyDirect({
524
+ account,
525
+ config,
526
+ target: `group:${groupCode}`,
527
+ msgBody: msgBody,
528
+ wsClient: ctx.wsClient,
529
+ core,
530
+ refMsgId,
531
+ refFromAccount: fromAccount,
532
+ });
521
533
  return { ok: result.ok, error: result.error };
522
534
  }
523
535
  catch (err) {
524
536
  const errMsg = err instanceof Error ? err.message : String(err);
525
537
  glog.error(`群 sendMediaOverride 失败: ${errMsg}`);
526
538
  const fallback = fallbackText ? `${fallbackText}\n${url}` : url;
527
- return sendYuanbaoGroupMessage({ account, groupCode, text: fallback, fromAccount: outboundSender, refMsgId, ctx });
539
+ return sendYuanbaoGroupMessage({
540
+ account,
541
+ groupCode,
542
+ text: fallback,
543
+ fromAccount: outboundSender,
544
+ refMsgId,
545
+ refFromAccount: fromAccount,
546
+ ctx,
547
+ });
528
548
  }
529
549
  };
530
550
  groupQueueManager.registerSession(outboundGroupSessionKey, {
@@ -534,6 +554,7 @@ async function handleGroupMessage(params) {
534
554
  target: groupCode,
535
555
  fromAccount: outboundSender,
536
556
  refMsgId,
557
+ refFromAccount: fromAccount,
537
558
  ctx,
538
559
  sendMediaOverride: groupSendMediaOverride,
539
560
  mergeOnFlush: account.disableBlockStreaming,
@@ -32,6 +32,7 @@ export declare function sendYuanbaoGroupMessageBody(params: {
32
32
  msgBody: YuanbaoMsgBodyElement[];
33
33
  fromAccount?: string;
34
34
  refMsgId?: string;
35
+ refFromAccount?: string;
35
36
  ctx?: MessageHandlerContext;
36
37
  }): Promise<{
37
38
  ok: boolean;
@@ -45,6 +46,7 @@ export declare function sendYuanbaoGroupMessage(params: {
45
46
  text: string;
46
47
  fromAccount?: string;
47
48
  refMsgId?: string;
49
+ refFromAccount?: string;
48
50
  ctx?: MessageHandlerContext;
49
51
  }): Promise<{
50
52
  ok: boolean;
@@ -58,6 +60,7 @@ export declare function sendMsgBodyDirect(params: {
58
60
  target: string;
59
61
  msgBody: YuanbaoMsgBodyElement[];
60
62
  refMsgId?: string;
63
+ refFromAccount?: string;
61
64
  wsClient: YuanbaoWsClient;
62
65
  core: PluginRuntime;
63
66
  }): Promise<{
@@ -8,13 +8,18 @@ const firstReplyRefDb = new InMemoryTtlDb({
8
8
  ttlMs: 60 * 1000,
9
9
  maxKeys: 100,
10
10
  });
11
- function shouldAttachReplyRef(params) {
12
- const { account, refMsgId } = params;
11
+ async function shouldAttachReplyRef(params) {
12
+ const { account, refMsgId, groupCode, refFromAccount } = params;
13
13
  if (!refMsgId)
14
14
  return false;
15
15
  const mode = account.replyToMode;
16
16
  if (mode === 'off')
17
17
  return false;
18
+ if (refFromAccount) {
19
+ const yuanbaoUserId = await getMember(account.accountId).queryYuanbaoUserId(groupCode);
20
+ if (yuanbaoUserId && refFromAccount === yuanbaoUserId)
21
+ return false;
22
+ }
18
23
  if (mode === 'all')
19
24
  return true;
20
25
  const dedupeKey = `${account.accountId}:${refMsgId}`;
@@ -60,14 +65,14 @@ export async function sendYuanbaoMessage(params) {
60
65
  return sendYuanbaoMessageBody({ ...rest, msgBody });
61
66
  }
62
67
  export async function sendYuanbaoGroupMessageBody(params) {
63
- const { account, groupCode, msgBody, fromAccount, refMsgId, ctx } = params;
68
+ const { account, groupCode, msgBody, fromAccount, refMsgId, refFromAccount, ctx } = params;
64
69
  const log = createLog('outbound', ctx?.log);
65
70
  if (!ctx?.wsClient) {
66
71
  log.error('发送群消息失败: WebSocket 客户端不可用');
67
72
  return { ok: false, error: 'wsClient not available' };
68
73
  }
69
74
  const msgRandom = String(Math.floor(Math.random() * 4294967295));
70
- const attachReplyRef = shouldAttachReplyRef({ account, refMsgId });
75
+ const attachReplyRef = await shouldAttachReplyRef({ account, refMsgId, groupCode, refFromAccount });
71
76
  try {
72
77
  const result = await ctx.wsClient.sendGroupMessage({
73
78
  msg_id: refMsgId,
@@ -97,7 +102,7 @@ export async function sendYuanbaoGroupMessage(params) {
97
102
  return sendYuanbaoGroupMessageBody({ ...rest, groupCode, msgBody });
98
103
  }
99
104
  export async function sendMsgBodyDirect(params) {
100
- const { account, config, target, msgBody, wsClient, core, refMsgId } = params;
105
+ const { account, config, target, msgBody, wsClient, core, refMsgId, refFromAccount } = params;
101
106
  if (target?.startsWith('group:')) {
102
107
  const groupCode = target.slice('group:'.length);
103
108
  return sendYuanbaoGroupMessageBody({
@@ -106,6 +111,7 @@ export async function sendMsgBodyDirect(params) {
106
111
  msgBody,
107
112
  fromAccount: account.botId,
108
113
  refMsgId,
114
+ refFromAccount,
109
115
  ctx: {
110
116
  account,
111
117
  config,
@@ -144,6 +150,7 @@ export async function executeReply(params) {
144
150
  : null;
145
151
  const collectedTexts = [];
146
152
  let hasFinalInfo = false;
153
+ let hasQueuedContent = false;
147
154
  await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
148
155
  ctx: ctxPayload,
149
156
  cfg: replyRuntime.config,
@@ -165,11 +172,13 @@ export async function executeReply(params) {
165
172
  const text = core.channel.text.convertMarkdownTables(payload.text ?? '', tableMode);
166
173
  if (session) {
167
174
  if (text.trim()) {
175
+ hasQueuedContent = true;
168
176
  await session.push({ type: 'text', text });
169
177
  }
170
178
  const mediaUrls = payload.mediaUrls ?? [];
171
179
  for (const mediaUrl of mediaUrls) {
172
180
  if (mediaUrl) {
181
+ hasQueuedContent = true;
173
182
  await session.push({ type: 'media', mediaUrl });
174
183
  }
175
184
  }
@@ -191,6 +200,17 @@ export async function executeReply(params) {
191
200
  });
192
201
  if (session) {
193
202
  await session.flush();
203
+ if (!hasQueuedContent) {
204
+ const { fallbackReply } = account;
205
+ if (fallbackReply) {
206
+ rlog.info(`[${L}] AI 未返回回复内容(队列模式),使用兜底回复`);
207
+ await sendTextReply({ text: fallbackReply, transport, ctx, overflowPolicy, splitFinalText, L, log: rlog });
208
+ }
209
+ else {
210
+ rlog.warn(`[${L}] AI 未返回任何回复内容(队列模式)`);
211
+ }
212
+ return;
213
+ }
194
214
  ctx.statusSink?.({ lastOutboundAt: Date.now() });
195
215
  return;
196
216
  }
@@ -49,6 +49,7 @@ export declare class Member {
49
49
  readonly accountId: string;
50
50
  readonly session: SessionMember;
51
51
  readonly group: GroupMember;
52
+ private yuanbaoUserIdCache;
52
53
  constructor(accountId: string);
53
54
  recordUser(groupCode: string, userId: string, nickName: string): void;
54
55
  queryMembers(groupCode: string, nameFilter?: string): Promise<UserRecord[]>;
@@ -56,6 +57,7 @@ export declare class Member {
56
57
  lookupUserByNickName(groupCode: string, nickName: string): UserRecord | undefined;
57
58
  queryGroupOwner(groupCode: string): Promise<GroupOwnerInfo | null>;
58
59
  queryGroupInfo(groupCode: string): Promise<GroupInfoData | null>;
60
+ queryYuanbaoUserId(groupCode?: string): Promise<string | null>;
59
61
  listGroupCodes(): string[];
60
62
  formatRecords(records: UserRecord[]): FormattedUserRecord[];
61
63
  }
@@ -240,6 +240,7 @@ export class Member {
240
240
  accountId;
241
241
  session = new SessionMember();
242
242
  group;
243
+ yuanbaoUserIdCache = null;
243
244
  constructor(accountId) {
244
245
  this.accountId = accountId;
245
246
  this.group = new GroupMember(accountId, this.session);
@@ -276,6 +277,23 @@ export class Member {
276
277
  async queryGroupInfo(groupCode) {
277
278
  return this.group.queryGroupInfo(groupCode);
278
279
  }
280
+ async queryYuanbaoUserId(groupCode) {
281
+ if (this.yuanbaoUserIdCache)
282
+ return this.yuanbaoUserIdCache;
283
+ if (!groupCode) {
284
+ logger.debug?.('[member] queryYuanbaoUserId skipped: no cache and no groupCode');
285
+ return null;
286
+ }
287
+ const members = await this.group.getMembers(groupCode);
288
+ const yuanbao = members.find(u => u.userType === 2) ?? members.find(u => u.userType === 3);
289
+ if (!yuanbao?.userId) {
290
+ logger.warn?.(`[member] queryYuanbaoUserId failed: no yuanbao/bot found in group=${groupCode}`);
291
+ return null;
292
+ }
293
+ this.yuanbaoUserIdCache = yuanbao.userId;
294
+ logger.info?.(`[member] cached yuanbaoUserId=${yuanbao.userId} from group=${groupCode}`);
295
+ return this.yuanbaoUserIdCache;
296
+ }
279
297
  listGroupCodes() {
280
298
  return this.session.listGroupCodes();
281
299
  }
@@ -34,6 +34,7 @@ export interface RegisterSessionOptions {
34
34
  target: string;
35
35
  fromAccount?: string;
36
36
  refMsgId?: string;
37
+ refFromAccount?: string;
37
38
  ctx: MessageHandlerContext;
38
39
  msgId: string;
39
40
  sendMediaOverride?: SendMediaFn;
@@ -34,11 +34,19 @@ function createManager(config) {
34
34
  existing.abort();
35
35
  }
36
36
  const onComplete = () => sessions.delete(sessionKey);
37
- const { chatType, account, target, fromAccount, refMsgId, ctx, msgId, sendMediaOverride, mergeOnFlush } = options;
37
+ const { chatType, account, target, fromAccount, refMsgId, refFromAccount, ctx, msgId, sendMediaOverride, mergeOnFlush, } = options;
38
38
  const sendText = async (text) => {
39
39
  try {
40
40
  const result = chatType === 'group'
41
- ? await sendYuanbaoGroupMessage({ account, groupCode: target, text, fromAccount, refMsgId, ctx })
41
+ ? await sendYuanbaoGroupMessage({
42
+ account,
43
+ groupCode: target,
44
+ text,
45
+ fromAccount,
46
+ refMsgId,
47
+ refFromAccount,
48
+ ctx,
49
+ })
42
50
  : await sendYuanbaoMessage({ account, toAccount: target, text, fromAccount, ctx });
43
51
  return { ok: result.ok, error: result.error };
44
52
  }
@@ -20,6 +20,7 @@ export type YuanbaoAccountConfig = {
20
20
  mediaMaxMb?: number;
21
21
  historyLimit?: number;
22
22
  disableBlockStreaming?: boolean;
23
+ requireMention?: boolean;
23
24
  fallbackReply?: string;
24
25
  };
25
26
  export type YuanbaoConfig = YuanbaoAccountConfig & {
@@ -46,6 +47,7 @@ export type ResolvedYuanbaoAccount = {
46
47
  mediaMaxMb: number;
47
48
  historyLimit: number;
48
49
  disableBlockStreaming: boolean;
50
+ requireMention: boolean;
49
51
  fallbackReply?: string;
50
52
  config: YuanbaoAccountConfig;
51
53
  };
@@ -2,7 +2,7 @@
2
2
  "id": "openclaw-plugin-yuanbao",
3
3
  "name": "元宝 Bot",
4
4
  "description": "Tencent YuanBao intelligent bot channel plugin",
5
- "version": "2.0.1",
5
+ "version": "2.1.0",
6
6
  "channels": [
7
7
  "yuanbao"
8
8
  ],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openclaw-plugin-yuanbao",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "description": "Tencent YuanBao intelligent bot channel plugin",
6
6
  "license": "MIT",
@@ -21,6 +21,7 @@
21
21
  "extensions": [
22
22
  "./dist/index.js"
23
23
  ],
24
+ "hooks": [],
24
25
  "channel": {
25
26
  "id": "yuanbao",
26
27
  "label": "元宝 Bot",