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.
- package/dist/openclaw.plugin.json +1 -1
- package/dist/src/accounts.js +3 -1
- package/dist/src/config-schema.js +7 -0
- package/dist/src/message-handler/chat-history.d.ts +1 -1
- package/dist/src/message-handler/inbound.js +25 -4
- package/dist/src/message-handler/outbound.d.ts +3 -0
- package/dist/src/message-handler/outbound.js +25 -5
- package/dist/src/module/member.d.ts +2 -0
- package/dist/src/module/member.js +18 -0
- package/dist/src/outbound-queue.d.ts +1 -0
- package/dist/src/outbound-queue.js +10 -2
- package/dist/src/types.d.ts +2 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +2 -1
package/dist/src/accounts.js
CHANGED
|
@@ -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
|
|
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 { recordPendingHistoryEntryIfEnabled, buildPendingHistoryContextFromMap, clearHistoryEntriesIfEnabled, } from 'openclaw/plugin-sdk/
|
|
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
|
-
|
|
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({
|
|
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({
|
|
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,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({
|
|
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
|
}
|
package/dist/src/types.d.ts
CHANGED
|
@@ -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
|
};
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-plugin-yuanbao",
|
|
3
|
-
"version": "2.0
|
|
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",
|