dlinkclaw-openclaw 1.0.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/LICENSE +201 -0
- package/README.en.md +46 -0
- package/README.md +15 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +1 -0
- package/dist/package.json +95 -0
- package/dist/src/account.d.ts +54 -0
- package/dist/src/account.js +394 -0
- package/dist/src/channel.d.ts +17 -0
- package/dist/src/channel.js +153 -0
- package/dist/src/client/client-manager.d.ts +4 -0
- package/dist/src/client/client-manager.js +10 -0
- package/dist/src/client/const.d.ts +37 -0
- package/dist/src/client/const.js +155 -0
- package/dist/src/client/crypto.d.ts +47 -0
- package/dist/src/client/crypto.js +260 -0
- package/dist/src/client/dl-client.d.ts +117 -0
- package/dist/src/client/dl-client.js +588 -0
- package/dist/src/client/dl-http.d.ts +27 -0
- package/dist/src/client/dl-http.js +223 -0
- package/dist/src/client/media-handler.d.ts +21 -0
- package/dist/src/client/media-handler.js +196 -0
- package/dist/src/client/message.d.ts +96 -0
- package/dist/src/client/message.js +1211 -0
- package/dist/src/client/timeout.d.ts +20 -0
- package/dist/src/client/timeout.js +36 -0
- package/dist/src/client/types.d.ts +226 -0
- package/dist/src/client/types.js +1 -0
- package/dist/src/monitor.d.ts +18 -0
- package/dist/src/monitor.js +322 -0
- package/dist/src/outbound.d.ts +6 -0
- package/dist/src/outbound.js +78 -0
- package/dist/src/policy.d.ts +42 -0
- package/dist/src/policy.js +216 -0
- package/dist/src/runtime.d.ts +9 -0
- package/dist/src/runtime.js +65 -0
- package/dist/src/services/gateway-rpc.d.ts +37 -0
- package/dist/src/services/gateway-rpc.js +37 -0
- package/dist/src/services/index.d.ts +41 -0
- package/dist/src/services/index.js +245 -0
- package/dist/src/services/services.d.ts +169 -0
- package/dist/src/services/services.js +1160 -0
- package/dist/src/services/types.d.ts +391 -0
- package/dist/src/services/types.js +5 -0
- package/dist/src/services/ws-client.d.ts +186 -0
- package/dist/src/services/ws-client.js +896 -0
- package/dist/src/utils.d.ts +20 -0
- package/dist/src/utils.js +128 -0
- package/ext_capabilities/workrelay-speak/CLONE_GUIDE.md +129 -0
- package/ext_capabilities/workrelay-speak/HARDWARE.md +84 -0
- package/ext_capabilities/workrelay-speak/QWEN_GUIDE.md +151 -0
- package/ext_capabilities/workrelay-speak/README.md +79 -0
- package/ext_capabilities/workrelay-speak/__pycache__/patch-kokoro-en.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-speak/install.sh +329 -0
- package/ext_capabilities/workrelay-speak/install.sh.bak-20260814 +376 -0
- package/ext_capabilities/workrelay-speak/manifest.json +63 -0
- package/ext_capabilities/workrelay-speak/patch-kokoro-en.py +85 -0
- package/ext_capabilities/workrelay-speak/status.sh +111 -0
- package/ext_capabilities/workrelay-speak/workrelay-tts.sh +118 -0
- package/ext_capabilities/workrelay-speak-luxtts/README.md +44 -0
- package/ext_capabilities/workrelay-speak-luxtts/install.sh +330 -0
- package/ext_capabilities/workrelay-speak-luxtts/manifest.json +63 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/45ea2e2c-0863-4966-a739-8bff8817f4c0.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/800e5af4-cba5-441e-8003-6d36f35409e6.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/85fafe01-d4d6-4a48-890e-9ae113f21905.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/reference_text.txt +4 -0
- package/ext_capabilities/workrelay-speak-luxtts/samples/stone-sample-6.4s.wav +0 -0
- package/ext_capabilities/workrelay-speak-luxtts/status.sh +115 -0
- package/ext_capabilities/workrelay-speak-luxtts/workrelay-tts.sh +98 -0
- package/ext_capabilities/workrelay-speak-qwen/README.md +62 -0
- package/ext_capabilities/workrelay-speak-qwen/install.sh +349 -0
- package/ext_capabilities/workrelay-speak-qwen/manifest.json +63 -0
- package/ext_capabilities/workrelay-speak-qwen/status.sh +106 -0
- package/ext_capabilities/workrelay-speak-qwen/workrelay-tts.sh +98 -0
- package/ext_capabilities/workrelay-transcribe/README.md +76 -0
- package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-transcribe/__pycache__/transcribe_server.cpython-312.pyc +0 -0
- package/ext_capabilities/workrelay-transcribe/install.sh +227 -0
- package/ext_capabilities/workrelay-transcribe/manifest.json +42 -0
- package/ext_capabilities/workrelay-transcribe/status.sh +63 -0
- package/ext_capabilities/workrelay-transcribe/transcribe.py +56 -0
- package/ext_capabilities/workrelay-transcribe/transcribe_server.py +116 -0
- package/ext_capabilities/workrelay-transcribe/voicebox-transcribe.sh +88 -0
- package/openclaw.plugin.json +142 -0
- package/package.json +95 -0
|
@@ -0,0 +1,1211 @@
|
|
|
1
|
+
import { MEDIA_DOCUMENT_PLACEHOLDER, MEDIA_IMAGE_PLACEHOLDER, MEDIA_VIDEO_PLACEHOLDER, MEDIA_VOICE_PLACEHOLDER } from "./const.js";
|
|
2
|
+
import { md5 } from "./crypto.js";
|
|
3
|
+
import { createChannelHistoryWindow } from "openclaw/plugin-sdk/reply-history";
|
|
4
|
+
import { buildChannelInboundEventContext } from "openclaw/plugin-sdk/channel-inbound";
|
|
5
|
+
import { uuidStr } from "../utils.js";
|
|
6
|
+
//入站队列最大数
|
|
7
|
+
const MESSAGE_QUEUE_SIZE = 2_000;
|
|
8
|
+
// Message accumulator: stores the accumulated payload and original message fields by streamId
|
|
9
|
+
const messageAccumulators = new Map();
|
|
10
|
+
//依不同的中继及通信方式获取最大等待时间(被动回复时间,如超过该时间则改用主动回复)
|
|
11
|
+
const getExpireTime = (botType, websocket) => {
|
|
12
|
+
switch (botType) {
|
|
13
|
+
case "ewx":
|
|
14
|
+
//企业微信Websocket被动回复最长24小时,Webhook 1小时
|
|
15
|
+
return websocket ? (24 * 60 * 60 * 1000) : (60 * 60 * 1000);
|
|
16
|
+
case "qq":
|
|
17
|
+
case "dd":
|
|
18
|
+
case "feishu":
|
|
19
|
+
case "dLink":
|
|
20
|
+
return 24 * 60 * 60 * 1000;
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* mqtt接收到消息后预处理
|
|
25
|
+
* @param msg
|
|
26
|
+
* @param logger
|
|
27
|
+
*/
|
|
28
|
+
export const messagePreviewHandler = (msg, accountId, logger) => {
|
|
29
|
+
logger.info(`account[${accountId}],[Message.ts(messagePreviewHandler)] message body:${msg.payload}`);
|
|
30
|
+
const now = Date.now();
|
|
31
|
+
//队列最多存放1000条消息,超过会移除最早一条
|
|
32
|
+
if (messageAccumulators.size >= MESSAGE_QUEUE_SIZE) {
|
|
33
|
+
const first = messageAccumulators.values().next().value;
|
|
34
|
+
messageAccumulators.delete(first.key);
|
|
35
|
+
logger.warn(`[messagePreviewHandler] account[${accountId}] The message [${first.key}] has been cleaned. `);
|
|
36
|
+
}
|
|
37
|
+
//清理超时队列
|
|
38
|
+
const expireList = Array.from(messageAccumulators.values()).filter(item => now >= item.expireTime);
|
|
39
|
+
if (expireList.length > 0) {
|
|
40
|
+
expireList.forEach(item => {
|
|
41
|
+
messageAccumulators.delete(item.key);
|
|
42
|
+
logger.warn(`[messagePreviewHandler] account[${accountId}] The message [${item.key}] has been cleaned. `);
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
//过滤重复的请求
|
|
46
|
+
if (messageAccumulators.has(msg.streamId)) {
|
|
47
|
+
logger.warn(`[messagePreviewHandler] account[${accountId}] The message has Multiple, Ignored`);
|
|
48
|
+
return {
|
|
49
|
+
key: msg.streamId,
|
|
50
|
+
accumulatedPayload: ''
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
//队列有效时间,超过该时间会自动被清理
|
|
54
|
+
const time = now + getExpireTime(msg.botType, msg.websocket);
|
|
55
|
+
//接收到的消息放入队列
|
|
56
|
+
messageAccumulators.set(msg.streamId, {
|
|
57
|
+
key: msg.streamId,
|
|
58
|
+
expireTime: time,
|
|
59
|
+
accumulatedPayload: '',
|
|
60
|
+
originalFields: {
|
|
61
|
+
toAgent: msg.toAgent,
|
|
62
|
+
fromPerson: msg.fromPerson,
|
|
63
|
+
msgId: msg.msgId,
|
|
64
|
+
botType: msg.botType,
|
|
65
|
+
contactType: msg.contactType,
|
|
66
|
+
contactId: msg.contactId,
|
|
67
|
+
notify: msg.notify,
|
|
68
|
+
notifyUrl: msg.notifyUrl,
|
|
69
|
+
chatId: msg.chatId,
|
|
70
|
+
group: msg.group,
|
|
71
|
+
groupIntroduction: msg.groupIntroduction,
|
|
72
|
+
reqId: msg.reqId,
|
|
73
|
+
websocket: msg.websocket,
|
|
74
|
+
askQuestion: msg.payload,
|
|
75
|
+
msgType: msg.msgType,
|
|
76
|
+
relayTimeRhreshold: time,
|
|
77
|
+
fromUsers: msg.fromUsers,
|
|
78
|
+
relayType: 'openClaw',
|
|
79
|
+
relayId: msg.relayId
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
msg.relayTimeRhreshold = time;
|
|
83
|
+
return {
|
|
84
|
+
key: msg.streamId,
|
|
85
|
+
accumulatedPayload: ''
|
|
86
|
+
};
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* 消息出站时构建回复消息
|
|
90
|
+
* @param param0
|
|
91
|
+
* @returns
|
|
92
|
+
*/
|
|
93
|
+
export const buildDLReplyMessage = ({ streamId, content, finish, kind, runId }) => {
|
|
94
|
+
const accumulator = messageAccumulators.get(streamId);
|
|
95
|
+
if (!accumulator) {
|
|
96
|
+
//已超过队例时间,已被清除了
|
|
97
|
+
return undefined;
|
|
98
|
+
}
|
|
99
|
+
if (runId && runId.length > 0) {
|
|
100
|
+
//在agent开发执行任务时首先会输出该runId
|
|
101
|
+
accumulator.originalFields.runId = runId; //更新runId, 需要回传到服务端
|
|
102
|
+
}
|
|
103
|
+
accumulator.accumulatedPayload = content;
|
|
104
|
+
const resultMessage = {
|
|
105
|
+
payload: accumulator.accumulatedPayload,
|
|
106
|
+
finish: finish,
|
|
107
|
+
streamId: streamId,
|
|
108
|
+
msgId: accumulator.originalFields.msgId,
|
|
109
|
+
fromPerson: accumulator.originalFields.fromPerson,
|
|
110
|
+
toAgent: accumulator.originalFields.toAgent,
|
|
111
|
+
botType: accumulator.originalFields.botType,
|
|
112
|
+
contactType: accumulator.originalFields.contactType,
|
|
113
|
+
contactId: accumulator.originalFields.contactId,
|
|
114
|
+
notify: accumulator.originalFields.notify,
|
|
115
|
+
notifyUrl: accumulator.originalFields.notifyUrl,
|
|
116
|
+
chatId: accumulator.originalFields.chatId,
|
|
117
|
+
group: accumulator.originalFields.group,
|
|
118
|
+
reqId: accumulator.originalFields.reqId,
|
|
119
|
+
websocket: accumulator.originalFields.websocket,
|
|
120
|
+
askQuestion: accumulator.originalFields.askQuestion,
|
|
121
|
+
msgType: accumulator.originalFields.msgType,
|
|
122
|
+
timestamp: Date.now(),
|
|
123
|
+
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
124
|
+
relayTimeRhreshold: accumulator.originalFields.relayTimeRhreshold,
|
|
125
|
+
fromUsers: accumulator.originalFields.fromUsers,
|
|
126
|
+
kind,
|
|
127
|
+
runId: accumulator.originalFields.runId,
|
|
128
|
+
relayType: 'openClaw',
|
|
129
|
+
relayId: accumulator.originalFields.relayId
|
|
130
|
+
};
|
|
131
|
+
if (finish) {
|
|
132
|
+
messageAccumulators.delete(streamId);
|
|
133
|
+
}
|
|
134
|
+
return resultMessage;
|
|
135
|
+
};
|
|
136
|
+
/**
|
|
137
|
+
* 解析消息内容(支持单条消息、图文混排和引用消息)
|
|
138
|
+
* @returns 提取的文本数组、图片URL数组和引用消息内容
|
|
139
|
+
*/
|
|
140
|
+
export function parseMessageContent(payload) {
|
|
141
|
+
const textParts = [];
|
|
142
|
+
const imageUrls = [];
|
|
143
|
+
const imageAesKeys = new Map();
|
|
144
|
+
const voiceUrls = [];
|
|
145
|
+
const voiceAesKeys = new Map();
|
|
146
|
+
const videoUrls = [];
|
|
147
|
+
const videoAesKeys = new Map();
|
|
148
|
+
const fileUrls = [];
|
|
149
|
+
const fileAesKeys = new Map();
|
|
150
|
+
let quoteContent;
|
|
151
|
+
let msgBody;
|
|
152
|
+
try {
|
|
153
|
+
msgBody = JSON.parse(payload);
|
|
154
|
+
}
|
|
155
|
+
catch (e) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const msgtype = msgBody.msgType;
|
|
159
|
+
// 处理图文混排消息
|
|
160
|
+
switch (msgtype) {
|
|
161
|
+
case "text":
|
|
162
|
+
const textBody = msgBody;
|
|
163
|
+
if (textBody.text && textBody.text.length > 0) {
|
|
164
|
+
textParts.push(textBody.text);
|
|
165
|
+
}
|
|
166
|
+
const textQuote = textBody.quote;
|
|
167
|
+
if (textQuote) {
|
|
168
|
+
quoteContent = resolveQuote({
|
|
169
|
+
quote: textQuote,
|
|
170
|
+
quoteContent,
|
|
171
|
+
imageUrls,
|
|
172
|
+
imageAesKeys,
|
|
173
|
+
voiceUrls,
|
|
174
|
+
voiceAesKeys,
|
|
175
|
+
videoUrls,
|
|
176
|
+
videoAesKeys,
|
|
177
|
+
fileUrls,
|
|
178
|
+
fileAesKeys
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
break;
|
|
182
|
+
case "image":
|
|
183
|
+
const imageBody = msgBody;
|
|
184
|
+
if (imageBody.imageUrl && imageBody.imageUrl.length > 0) {
|
|
185
|
+
imageUrls.push(imageBody.imageUrl);
|
|
186
|
+
if (imageBody.aesKey && imageBody.aesKey.length > 0) {
|
|
187
|
+
imageAesKeys.set(md5(imageBody.imageUrl), imageBody.aesKey);
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
break;
|
|
191
|
+
case "voice":
|
|
192
|
+
const voiceBody = msgBody;
|
|
193
|
+
if (voiceBody.voiceUrl && voiceBody.voiceUrl.length > 0) {
|
|
194
|
+
voiceUrls.push(voiceBody.voiceUrl);
|
|
195
|
+
if (voiceBody.aesKey && voiceBody.aesKey.length > 0) {
|
|
196
|
+
voiceAesKeys.set(md5(voiceBody.voiceUrl), voiceBody.aesKey);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
break;
|
|
200
|
+
case "video":
|
|
201
|
+
const videoBody = msgBody;
|
|
202
|
+
if (videoBody.videoUrl && videoBody.videoUrl.length > 0) {
|
|
203
|
+
videoUrls.push(videoBody.videoUrl);
|
|
204
|
+
if (videoBody.aesKey && videoBody.aesKey.length > 0) {
|
|
205
|
+
videoAesKeys.set(md5(videoBody.videoUrl), videoBody.aesKey);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
break;
|
|
209
|
+
case "file":
|
|
210
|
+
const fileBody = msgBody;
|
|
211
|
+
if (fileBody.fileUrl && fileBody.fileUrl.length > 0) {
|
|
212
|
+
fileUrls.push(fileBody.fileUrl);
|
|
213
|
+
if (fileBody.aesKey && fileBody.aesKey.length > 0) {
|
|
214
|
+
fileAesKeys.set(md5(fileBody.fileUrl), fileBody.aesKey);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
break;
|
|
218
|
+
case "mixed":
|
|
219
|
+
const mixedBody = msgBody;
|
|
220
|
+
for (const item of mixedBody.msgList) {
|
|
221
|
+
switch (item.msgType) {
|
|
222
|
+
case "text":
|
|
223
|
+
const text = item.text;
|
|
224
|
+
if (text && text.length > 0) {
|
|
225
|
+
textParts.push(text);
|
|
226
|
+
}
|
|
227
|
+
break;
|
|
228
|
+
case "image":
|
|
229
|
+
const image = item;
|
|
230
|
+
if (image.imageUrl && image.imageUrl.length > 0) {
|
|
231
|
+
imageUrls.push(image.imageUrl);
|
|
232
|
+
if (image.aesKey && image.aesKey.length > 0) {
|
|
233
|
+
imageAesKeys.set(md5(image.imageUrl), image.aesKey);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
break;
|
|
237
|
+
case "voice":
|
|
238
|
+
const voice = item;
|
|
239
|
+
if (voice.voiceUrl && voice.voiceUrl.length > 0) {
|
|
240
|
+
voiceUrls.push(voice.voiceUrl);
|
|
241
|
+
if (voice.aesKey && voice.aesKey.length > 0) {
|
|
242
|
+
voiceAesKeys.set(md5(voice.voiceUrl), voice.aesKey);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
break;
|
|
246
|
+
case "video":
|
|
247
|
+
const video = item;
|
|
248
|
+
if (video.videoUrl && video.videoUrl.length > 0) {
|
|
249
|
+
videoUrls.push(video.videoUrl);
|
|
250
|
+
if (video.aesKey && video.aesKey.length > 0) {
|
|
251
|
+
videoAesKeys.set(md5(video.videoUrl), video.aesKey);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
case "file":
|
|
256
|
+
const file = item;
|
|
257
|
+
if (file.fileUrl && file.fileUrl.length > 0) {
|
|
258
|
+
fileUrls.push(file.fileUrl);
|
|
259
|
+
if (file.aesKey && file.aesKey.length > 0) {
|
|
260
|
+
fileAesKeys.set(md5(file.fileUrl), file.aesKey);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
const quote = mixedBody.quote;
|
|
266
|
+
if (quote) {
|
|
267
|
+
quoteContent = resolveQuote({
|
|
268
|
+
quote,
|
|
269
|
+
quoteContent,
|
|
270
|
+
imageUrls,
|
|
271
|
+
imageAesKeys,
|
|
272
|
+
voiceUrls,
|
|
273
|
+
voiceAesKeys,
|
|
274
|
+
videoUrls,
|
|
275
|
+
videoAesKeys,
|
|
276
|
+
fileUrls,
|
|
277
|
+
fileAesKeys
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
break;
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
textParts,
|
|
284
|
+
imageUrls,
|
|
285
|
+
imageAesKeys,
|
|
286
|
+
voiceUrls,
|
|
287
|
+
voiceAesKeys,
|
|
288
|
+
videoUrls,
|
|
289
|
+
videoAesKeys,
|
|
290
|
+
fileUrls,
|
|
291
|
+
fileAesKeys,
|
|
292
|
+
quoteContent
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
function resolveQuote(params) {
|
|
296
|
+
const { quote, quoteContent, imageUrls, imageAesKeys, voiceUrls, voiceAesKeys, videoUrls, videoAesKeys, fileUrls, fileAesKeys } = params;
|
|
297
|
+
let content = quoteContent ?? '';
|
|
298
|
+
if (quote.type === 'text') {
|
|
299
|
+
content = quote.text;
|
|
300
|
+
}
|
|
301
|
+
else if (quote.type === 'image') {
|
|
302
|
+
const body = quote;
|
|
303
|
+
if (body.imageUrl && body.imageUrl.length > 0) {
|
|
304
|
+
imageUrls.push(body.imageUrl);
|
|
305
|
+
if (body.aesKey && body.aesKey.length > 0) {
|
|
306
|
+
imageAesKeys.set(body.imageUrl, body.aesKey);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
else if (quote.type === 'voice') {
|
|
311
|
+
const body = quote;
|
|
312
|
+
if (body.voiceUrl && body.voiceUrl.length > 0) {
|
|
313
|
+
voiceUrls.push(body.voiceUrl);
|
|
314
|
+
if (body.aesKey && body.aesKey.length > 0) {
|
|
315
|
+
voiceAesKeys.set(body.voiceUrl, body.aesKey);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
else if (quote.type === 'video') {
|
|
320
|
+
const body = quote;
|
|
321
|
+
if (body.videoUrl && body.videoUrl.length > 0) {
|
|
322
|
+
videoUrls.push(body.videoUrl);
|
|
323
|
+
if (body.aesKey && body.aesKey.length > 0) {
|
|
324
|
+
videoAesKeys.set(body.videoUrl, body.aesKey);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
else if (quote.type === 'file') {
|
|
329
|
+
const body = quote;
|
|
330
|
+
if (body.fileUrl && body.fileUrl.length > 0) {
|
|
331
|
+
fileUrls.push(body.fileUrl);
|
|
332
|
+
if (body.aesKey && body.aesKey.length > 0) {
|
|
333
|
+
fileAesKeys.set(body.fileUrl, body.aesKey);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else if (quote.type === 'mixed') {
|
|
338
|
+
const items = quote.msgList ?? [];
|
|
339
|
+
for (const item of items) {
|
|
340
|
+
if (item.msgType === 'text') {
|
|
341
|
+
const body = item;
|
|
342
|
+
if (body.text && body.text.length > 0) {
|
|
343
|
+
content += body.text;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
else if (item.msgType === 'image') {
|
|
347
|
+
const body = item;
|
|
348
|
+
if (body.imageUrl && body.imageUrl.length > 0) {
|
|
349
|
+
imageUrls.push(body.imageUrl);
|
|
350
|
+
if (body.aesKey && body.aesKey.length > 0) {
|
|
351
|
+
imageAesKeys.set(body.imageUrl, body.aesKey);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return content;
|
|
358
|
+
}
|
|
359
|
+
async function resetSession({ client, sessionKey, streamId, agentId }) {
|
|
360
|
+
//拦截/new,/reset 指令
|
|
361
|
+
//获取历史会话记录
|
|
362
|
+
const histories = await client.exeCmd({
|
|
363
|
+
cmd: 'chat.history',
|
|
364
|
+
requestId: uuidStr(36),
|
|
365
|
+
timestamp: Date.now(),
|
|
366
|
+
params: {
|
|
367
|
+
sessionKey,
|
|
368
|
+
limit: 100
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
if (!histories?.success || !histories.data) {
|
|
372
|
+
client.publishToServer({
|
|
373
|
+
streamId,
|
|
374
|
+
content: `[tag_error_session_history_fail]${histories?.error ?? ''}`,
|
|
375
|
+
finish: true,
|
|
376
|
+
kind: 'block'
|
|
377
|
+
});
|
|
378
|
+
return true;
|
|
379
|
+
}
|
|
380
|
+
//const sid: string = histories.data.sessionId;
|
|
381
|
+
const historyRecords = histories.data.messages ?? [];
|
|
382
|
+
const msgList = historyRecords.map((item) => {
|
|
383
|
+
const content = item.content;
|
|
384
|
+
if (typeof content === 'string') {
|
|
385
|
+
return `[${item.role}] ${item.content}`;
|
|
386
|
+
}
|
|
387
|
+
const items = content.map((msg) => {
|
|
388
|
+
try {
|
|
389
|
+
if (msg.type === 'text') {
|
|
390
|
+
return msg.text;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
}
|
|
395
|
+
return '';
|
|
396
|
+
});
|
|
397
|
+
const newItems = items.filter(rec => rec.length > 0);
|
|
398
|
+
return `[${item.role}] ${newItems.join(';')}`;
|
|
399
|
+
});
|
|
400
|
+
if (msgList.length === 0) {
|
|
401
|
+
client.publishToServer({
|
|
402
|
+
streamId,
|
|
403
|
+
content: '[tag_error_session_record_empty]',
|
|
404
|
+
finish: true,
|
|
405
|
+
kind: 'block'
|
|
406
|
+
});
|
|
407
|
+
return true;
|
|
408
|
+
}
|
|
409
|
+
const resetResult = await client.exeCmd({
|
|
410
|
+
cmd: 'sessions.reset',
|
|
411
|
+
requestId: uuidStr(36),
|
|
412
|
+
timestamp: Date.now(),
|
|
413
|
+
params: {
|
|
414
|
+
key: sessionKey,
|
|
415
|
+
reason: "new"
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
if (!resetResult?.success || !resetResult.data) {
|
|
419
|
+
client.publishToServer({
|
|
420
|
+
streamId,
|
|
421
|
+
content: '[tag_error_reset_session_fail]' + (resetResult?.error || ''),
|
|
422
|
+
kind: 'block',
|
|
423
|
+
finish: true,
|
|
424
|
+
});
|
|
425
|
+
return true;
|
|
426
|
+
}
|
|
427
|
+
const prompt = `请根据以下聊天记录,生成一个不超过128字的标题,概括这次对话的主题:
|
|
428
|
+
${msgList.join('\n')}
|
|
429
|
+
|
|
430
|
+
要求:
|
|
431
|
+
- 标题长度 ≤ 128 字
|
|
432
|
+
- 简洁准确地概括对话核心内容
|
|
433
|
+
- 直接输出标题,并在标题末尾符加上标记"[tag_reset]",不要多余文字`;
|
|
434
|
+
const askAgent = await client.exeCmd({
|
|
435
|
+
cmd: 'agent.ask',
|
|
436
|
+
requestId: uuidStr(36),
|
|
437
|
+
timestamp: Date.now(),
|
|
438
|
+
params: {
|
|
439
|
+
agentId,
|
|
440
|
+
message: prompt,
|
|
441
|
+
sessionKey,
|
|
442
|
+
deliver: false, //表示出站时不走deliver
|
|
443
|
+
idempotencyKey: uuidStr(36)
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
let askAgentError = '';
|
|
447
|
+
if (!askAgent?.success || !askAgent.data) {
|
|
448
|
+
askAgentError = `[tag_error_summarize_session_history_fail]\n ${askAgent?.error}`;
|
|
449
|
+
}
|
|
450
|
+
const status = askAgent?.data.status;
|
|
451
|
+
if (status !== 'accepted') {
|
|
452
|
+
askAgentError = `${agentId} [tag_error_reject_session_history]`;
|
|
453
|
+
}
|
|
454
|
+
if (askAgentError && askAgentError.length > 0) {
|
|
455
|
+
client.publishToServer({
|
|
456
|
+
streamId,
|
|
457
|
+
content: askAgentError,
|
|
458
|
+
kind: 'block',
|
|
459
|
+
finish: true,
|
|
460
|
+
});
|
|
461
|
+
return true;
|
|
462
|
+
}
|
|
463
|
+
const runId = askAgent?.data.runId;
|
|
464
|
+
const new_sessionId = resetResult.data.entry.sessionId;
|
|
465
|
+
if (new_sessionId) {
|
|
466
|
+
client.publishToServer({
|
|
467
|
+
streamId,
|
|
468
|
+
content: '[tag_new_session_success]',
|
|
469
|
+
finish: true,
|
|
470
|
+
kind: 'block',
|
|
471
|
+
session: {
|
|
472
|
+
sessionKey,
|
|
473
|
+
runId,
|
|
474
|
+
//sessionId: new_sessionId,
|
|
475
|
+
archived: false
|
|
476
|
+
}
|
|
477
|
+
});
|
|
478
|
+
return true;
|
|
479
|
+
}
|
|
480
|
+
return false;
|
|
481
|
+
}
|
|
482
|
+
async function archiveSession({ client, sessionKey, streamId, agentId, chatId, chatType, channelId }) {
|
|
483
|
+
//获取历史会话记录
|
|
484
|
+
const histories = await client.exeCmd({
|
|
485
|
+
cmd: 'chat.history',
|
|
486
|
+
requestId: uuidStr(36),
|
|
487
|
+
timestamp: Date.now(),
|
|
488
|
+
params: {
|
|
489
|
+
sessionKey,
|
|
490
|
+
limit: 100
|
|
491
|
+
}
|
|
492
|
+
});
|
|
493
|
+
if (!histories?.success || !histories.data) {
|
|
494
|
+
client.publishToServer({
|
|
495
|
+
streamId,
|
|
496
|
+
content: `[tag_error_session_history_fail]${histories?.error ?? ''}`,
|
|
497
|
+
finish: true,
|
|
498
|
+
kind: 'block'
|
|
499
|
+
});
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const historyRecords = histories.data.messages ?? [];
|
|
503
|
+
const msgList = historyRecords.map((item) => {
|
|
504
|
+
const content = item.content;
|
|
505
|
+
if (typeof content === 'string') {
|
|
506
|
+
return `[${item.role}] ${item.content}`;
|
|
507
|
+
}
|
|
508
|
+
const items = content.map((msg) => {
|
|
509
|
+
try {
|
|
510
|
+
if (msg.type === 'text') {
|
|
511
|
+
return msg.text;
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
}
|
|
516
|
+
return '';
|
|
517
|
+
});
|
|
518
|
+
const newItems = items.filter(rec => rec.length > 0);
|
|
519
|
+
return `[${item.role}] ${newItems.join(';')}`;
|
|
520
|
+
});
|
|
521
|
+
if (msgList.length === 0) {
|
|
522
|
+
client.publishToServer({
|
|
523
|
+
streamId,
|
|
524
|
+
content: '[tag_error_session_record_empty]',
|
|
525
|
+
finish: true,
|
|
526
|
+
kind: 'block'
|
|
527
|
+
});
|
|
528
|
+
return;
|
|
529
|
+
}
|
|
530
|
+
//调用 sessions.archive 真正归档(不 reset,保留完整对话)
|
|
531
|
+
const archiveResult = await client.exeCmd({
|
|
532
|
+
cmd: 'sessions.archive',
|
|
533
|
+
requestId: uuidStr(36),
|
|
534
|
+
timestamp: Date.now(),
|
|
535
|
+
params: { sessionKey }
|
|
536
|
+
});
|
|
537
|
+
if (!archiveResult?.success || !archiveResult.data) {
|
|
538
|
+
client.publishToServer({
|
|
539
|
+
streamId,
|
|
540
|
+
content: '[tag_error_archive_session_fail]' + (archiveResult?.error || ''),
|
|
541
|
+
kind: 'block',
|
|
542
|
+
finish: true,
|
|
543
|
+
});
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
//自动创建新会话(带后缀),后续消息走新会话
|
|
547
|
+
const newSuffix = Date.now().toString(36);
|
|
548
|
+
const createResult = await client.exeCmd({
|
|
549
|
+
cmd: 'sessions.create',
|
|
550
|
+
requestId: uuidStr(36),
|
|
551
|
+
timestamp: Date.now(),
|
|
552
|
+
params: {
|
|
553
|
+
agentId,
|
|
554
|
+
userId: chatId,
|
|
555
|
+
chatType,
|
|
556
|
+
suffix: newSuffix
|
|
557
|
+
}
|
|
558
|
+
});
|
|
559
|
+
if (!createResult?.success || !createResult.data) {
|
|
560
|
+
client.publishToServer({
|
|
561
|
+
streamId,
|
|
562
|
+
content: '[tag_error_create_session_fail]' + (createResult?.error || ''),
|
|
563
|
+
kind: 'block',
|
|
564
|
+
finish: true,
|
|
565
|
+
});
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
const newSessionKey = `agent:${agentId}:${channelId}-${newSuffix}:${chatType}:${chatId.toLowerCase()}`;
|
|
569
|
+
//生成标题(在新的 agent 会话中执行)
|
|
570
|
+
const prompt = `请根据以下聊天记录,生成一个不超过128字的标题,概括这次对话的主题:
|
|
571
|
+
${msgList.join('\n')}
|
|
572
|
+
|
|
573
|
+
要求:
|
|
574
|
+
- 标题长度 ≤ 128 字
|
|
575
|
+
- 简洁准确地概括对话核心内容
|
|
576
|
+
- 直接输出标题,并在标题末尾符加上标记"[tag_archive]",不要多余文字`;
|
|
577
|
+
const askAgent = await client.exeCmd({
|
|
578
|
+
cmd: 'agent.ask',
|
|
579
|
+
requestId: uuidStr(36),
|
|
580
|
+
timestamp: Date.now(),
|
|
581
|
+
params: {
|
|
582
|
+
agentId,
|
|
583
|
+
message: prompt,
|
|
584
|
+
sessionKey,
|
|
585
|
+
deliver: false,
|
|
586
|
+
idempotencyKey: uuidStr(36)
|
|
587
|
+
}
|
|
588
|
+
});
|
|
589
|
+
let askAgentError = '';
|
|
590
|
+
if (!askAgent?.success || !askAgent.data) {
|
|
591
|
+
askAgentError = `[tag_error_summarize_session_history_fail]\n ${askAgent?.error}`;
|
|
592
|
+
}
|
|
593
|
+
const status = askAgent?.data.status;
|
|
594
|
+
if (status !== 'accepted') {
|
|
595
|
+
askAgentError = `${agentId} [tag_error_reject_session_history]`;
|
|
596
|
+
}
|
|
597
|
+
if (askAgentError && askAgentError.length > 0) {
|
|
598
|
+
client.publishToServer({
|
|
599
|
+
streamId,
|
|
600
|
+
content: askAgentError,
|
|
601
|
+
kind: 'block',
|
|
602
|
+
finish: true,
|
|
603
|
+
});
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
const runId = askAgent?.data.runId;
|
|
607
|
+
// 返回归档成功 + 新会话 key
|
|
608
|
+
client.publishToServer({
|
|
609
|
+
streamId,
|
|
610
|
+
content: '[tag_archive_success]',
|
|
611
|
+
finish: true,
|
|
612
|
+
kind: 'block',
|
|
613
|
+
session: {
|
|
614
|
+
sessionKey,
|
|
615
|
+
runId,
|
|
616
|
+
newSessionKey,
|
|
617
|
+
archived: true
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
export async function buildMessageContext(params) {
|
|
622
|
+
const { core, channelId, account, clawConfig, msg, imageList, voiceList, videoList, fileList, textBody, quoteContent, commandAuthorized, chatHistories, historyLimit, logger, client, streamId, relayId } = params;
|
|
623
|
+
const toAgent = msg.toAgent;
|
|
624
|
+
const fromUser = msg.fromPerson;
|
|
625
|
+
const isGroup = msg.group;
|
|
626
|
+
const groupIntroduction = msg.groupIntroduction;
|
|
627
|
+
const chatType = isGroup ? "group" : "direct";
|
|
628
|
+
// 构建会话标签
|
|
629
|
+
let chatId, fromLabel, from, to;
|
|
630
|
+
if (isGroup) {
|
|
631
|
+
chatId = msg.chatId;
|
|
632
|
+
fromLabel = `group:${chatId}-${toAgent.id}-${msg.botType}-${relayId}`;
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
chatId = fromUser.empId; //CorpEmployee.id
|
|
636
|
+
fromLabel = `user:${chatId}-${toAgent.id}-${msg.botType}-${relayId}`;
|
|
637
|
+
}
|
|
638
|
+
// 当客户端指定了 sessionKey 时,将其编码到 from/to 中(# 分隔),
|
|
639
|
+
// 便于 autoReply 主动出站时解析当前会话
|
|
640
|
+
const sessionLabel = (toAgent.sessionKey && toAgent.sessionKey.length > 0) ? `#${toAgent.sessionKey}` : '';
|
|
641
|
+
from = `${channelId}:${fromLabel}${sessionLabel}`;
|
|
642
|
+
to = `${channelId}:${fromLabel}${sessionLabel}`;
|
|
643
|
+
let messageBody = (textBody && textBody.length > 0) ? textBody : "";
|
|
644
|
+
const media_placeholder = [];
|
|
645
|
+
if (imageList.length > 0) {
|
|
646
|
+
media_placeholder.push(MEDIA_IMAGE_PLACEHOLDER);
|
|
647
|
+
}
|
|
648
|
+
if (voiceList.length > 0) {
|
|
649
|
+
media_placeholder.push(MEDIA_VOICE_PLACEHOLDER);
|
|
650
|
+
}
|
|
651
|
+
if (videoList.length > 0) {
|
|
652
|
+
media_placeholder.push(MEDIA_VIDEO_PLACEHOLDER);
|
|
653
|
+
}
|
|
654
|
+
if (fileList.length > 0) {
|
|
655
|
+
media_placeholder.push(MEDIA_DOCUMENT_PLACEHOLDER);
|
|
656
|
+
}
|
|
657
|
+
messageBody = media_placeholder.join('') + messageBody;
|
|
658
|
+
const mediaList = [...imageList, ...voiceList, ...videoList, ...fileList];
|
|
659
|
+
//会话路由
|
|
660
|
+
// let route = core.channel.routing.resolveAgentRoute({
|
|
661
|
+
// cfg: clawConfig,
|
|
662
|
+
// channel: channelId,
|
|
663
|
+
// accountId: account.accountId!,
|
|
664
|
+
// peer: {
|
|
665
|
+
// kind: chatType,
|
|
666
|
+
// id: chatId,
|
|
667
|
+
// },
|
|
668
|
+
// });
|
|
669
|
+
const agentId = toAgent.agentId;
|
|
670
|
+
// 客户端传了 sessionKey 则直接使用,否则自动构建默认会话
|
|
671
|
+
const sessionKey = toAgent.sessionKey || core.channel.routing.buildAgentSessionKey({
|
|
672
|
+
agentId: agentId,
|
|
673
|
+
channel: channelId,
|
|
674
|
+
accountId: account.accountId,
|
|
675
|
+
peer: {
|
|
676
|
+
kind: chatType,
|
|
677
|
+
id: chatId
|
|
678
|
+
},
|
|
679
|
+
dmScope: 'per-channel-peer'
|
|
680
|
+
});
|
|
681
|
+
//拦截 /archive 指令:真正归档当前会话 + 自动新建
|
|
682
|
+
if ("/archive" === messageBody.toLowerCase()) {
|
|
683
|
+
await archiveSession({
|
|
684
|
+
client,
|
|
685
|
+
sessionKey,
|
|
686
|
+
streamId,
|
|
687
|
+
agentId,
|
|
688
|
+
chatId,
|
|
689
|
+
chatType,
|
|
690
|
+
channelId
|
|
691
|
+
});
|
|
692
|
+
return undefined;
|
|
693
|
+
}
|
|
694
|
+
//拦截/new,/reset 指令
|
|
695
|
+
if (commandAuthorized && "/new" === messageBody.toLowerCase() || "/reset" === messageBody.toLowerCase()) {
|
|
696
|
+
const res = await resetSession({
|
|
697
|
+
client,
|
|
698
|
+
sessionKey,
|
|
699
|
+
streamId,
|
|
700
|
+
agentId
|
|
701
|
+
});
|
|
702
|
+
if (res) {
|
|
703
|
+
return undefined;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
//重启openclaw gateway restart
|
|
707
|
+
if (commandAuthorized && "/restart" === messageBody.toLowerCase()) {
|
|
708
|
+
await client.publishToServer({
|
|
709
|
+
streamId,
|
|
710
|
+
content: '[tag_gateway_restart]',
|
|
711
|
+
finish: true,
|
|
712
|
+
kind: 'block'
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
// 强制停止当前 Agent run(/stop 指令,比 /queue interrupt 更快)
|
|
716
|
+
// 新增 cmd 通道 /stop 指令
|
|
717
|
+
if (commandAuthorized && "/stop" === messageBody.toLowerCase()) {
|
|
718
|
+
await client.exeCmd({
|
|
719
|
+
cmd: 'agent.stop',
|
|
720
|
+
requestId: uuidStr(36),
|
|
721
|
+
timestamp: Date.now(),
|
|
722
|
+
params: {
|
|
723
|
+
agentId,
|
|
724
|
+
userId: chatId,
|
|
725
|
+
chatType
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
await client.publishToServer({
|
|
729
|
+
streamId,
|
|
730
|
+
content: '[tag_agent_stop]',
|
|
731
|
+
finish: true,
|
|
732
|
+
kind: 'block'
|
|
733
|
+
});
|
|
734
|
+
return undefined;
|
|
735
|
+
}
|
|
736
|
+
// Dynamic agent creation for DM users
|
|
737
|
+
// When enabled, creates a unique agent instance with its own workspace for each DM user.
|
|
738
|
+
// const config = account.config;
|
|
739
|
+
let effectiveCfg = clawConfig;
|
|
740
|
+
// if (!isGroup && route.matchedBy === "default") {
|
|
741
|
+
// const dynamicCfg = config?.dynamicAgentConfig;
|
|
742
|
+
// //是否为当前chatId创建动态agent
|
|
743
|
+
// if (dynamicCfg?.enabled) {
|
|
744
|
+
// const result = await maybeCreateDynamicAgent({
|
|
745
|
+
// cfg: clawConfig,
|
|
746
|
+
// runtime: core,
|
|
747
|
+
// senderId: chatId,
|
|
748
|
+
// channelId: channelId,
|
|
749
|
+
// dynamicCfg,
|
|
750
|
+
// log: (msg) => logger.info(msg),
|
|
751
|
+
// });
|
|
752
|
+
// if (result.created) {
|
|
753
|
+
// //如果创建成功,则当前来自chatId的请求使用该路由
|
|
754
|
+
// effectiveCfg = result.updatedCfg;
|
|
755
|
+
// // Re-resolve route with updated config
|
|
756
|
+
// route = core.channel.routing.resolveAgentRoute({
|
|
757
|
+
// cfg: result.updatedCfg,
|
|
758
|
+
// channel: channelId,
|
|
759
|
+
// accountId: account.accountId,
|
|
760
|
+
// peer: { kind: "direct", id: chatId },
|
|
761
|
+
// });
|
|
762
|
+
// logger.info(
|
|
763
|
+
// `${channelId} account[${account.accountId}]: dynamic agent created, new route: ${route.sessionKey}`
|
|
764
|
+
// );
|
|
765
|
+
// }
|
|
766
|
+
// }
|
|
767
|
+
// }
|
|
768
|
+
const preview = messageBody.replace(/\s+/g, " ").slice(0, 160);
|
|
769
|
+
const inboundLabel = `${channelId}:${account.accountId}:${chatId}`;
|
|
770
|
+
const isEnqueueEvent = core.system.enqueueSystemEvent(`${inboundLabel}:${preview}`, {
|
|
771
|
+
sessionKey: sessionKey, //route.sessionKey,
|
|
772
|
+
contextKey: `${channelId}:message:${msg.streamId}`,
|
|
773
|
+
});
|
|
774
|
+
const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(effectiveCfg);
|
|
775
|
+
// Include a readable speaker label so the model can attribute instructions.
|
|
776
|
+
// (DMs already have per-sender sessions, but the prefix is still useful for clarity.)
|
|
777
|
+
if (!messageBody.startsWith('/')) {
|
|
778
|
+
const speaker = `${chatId}`;
|
|
779
|
+
messageBody = `${speaker}:${messageBody}`;
|
|
780
|
+
await client.publishProcessingState({
|
|
781
|
+
streamId,
|
|
782
|
+
content: '[tag_start_ctx]',
|
|
783
|
+
status: 'read',
|
|
784
|
+
kind: 'block',
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
// If there are mention targets, inform the agent that replies will auto-mention them
|
|
788
|
+
if (isGroup) {
|
|
789
|
+
// Build a lookup-friendly member map
|
|
790
|
+
const groupMembers = msg.groupMembers ?? [];
|
|
791
|
+
if (groupMembers.length > 0) {
|
|
792
|
+
const memberMapStr = groupMembers
|
|
793
|
+
.map((t) => `${t.name} -> ${t.id} -> AgentId:${t.agentId ?? 'n/a'} -> Resp.:${t.responsibilities ?? 'unknown'} -> UserType:${t.type}`)
|
|
794
|
+
.join("\n");
|
|
795
|
+
messageBody += `\n\nSystem: This is a group chat. Group members (Nickname -> ID -> AgentId -> Responsibilities -> UserType):\n${memberMapStr}\n\n`;
|
|
796
|
+
messageBody += `When mentioning someone, use: @[Nickname](ID:UserType)\n`;
|
|
797
|
+
messageBody += `Example: @[John](12345:agent) Hello, please check this\n\n`;
|
|
798
|
+
messageBody += `Rules:\n`;
|
|
799
|
+
messageBody += `- If the user's request relates to a specific responsibility, prioritize mentioning the member(s) whose responsibility matches.`;
|
|
800
|
+
messageBody += `- ID must come from the member list above, do not fabricate\n`;
|
|
801
|
+
messageBody += `- If the member isn't in the list or ID is uncertain, do not use the @ format\n`;
|
|
802
|
+
messageBody += `- Always include the full @[Nickname](ID:UserType) format`;
|
|
803
|
+
// 2026-08-13 群组 Agent 协作:当群内有其他 Agent 成员(type=agent 且 agentId 非空)时,
|
|
804
|
+
// 告知当前 Agent 可以用 sessions_send 工具以自身身份向对方 Agent 发协作消息(isUser=false),
|
|
805
|
+
// 消息不会伪装成人类,对方 Agent 会把它当作 Agent 间协作消息处理。
|
|
806
|
+
// 目标 sessionKey 固定为群会话:agent:<对方agentId>:<channelId>:group:<chatId>
|
|
807
|
+
const agentMembers = groupMembers.filter((m) => m.type === 'agent' && m.agentId && m.agentId.length > 0);
|
|
808
|
+
if (agentMembers.length > 0) {
|
|
809
|
+
const agentRoutes = agentMembers
|
|
810
|
+
.map((m) => `- ${m.name} (agentId=${m.agentId}): sessions_send -> agent:${m.agentId}:${channelId}:group:${chatId.toLowerCase()}`)
|
|
811
|
+
.join("\n");
|
|
812
|
+
messageBody += `\n\nSystem: Group agent-to-agent collaboration:\n`;
|
|
813
|
+
messageBody += `You may collaborate with other Agent members in this group using the sessions_send tool. `;
|
|
814
|
+
messageBody += `When you need another Agent's input, call sessions_send with its group session key below; `;
|
|
815
|
+
messageBody += `the message arrives as an inter-agent message (isUser=false), NOT as a human instruction. `;
|
|
816
|
+
messageBody += `Your final reply is still broadcast to the whole group as usual. Routes:\n${agentRoutes}\n`;
|
|
817
|
+
}
|
|
818
|
+
if (groupIntroduction && groupIntroduction.length > 0) {
|
|
819
|
+
messageBody += `\n\nSystem: Below is an introduction to this group:\n`;
|
|
820
|
+
messageBody += groupIntroduction;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
// logger.info(
|
|
825
|
+
// `${channelId} account[${account.accountId}]: message body: ${messageBody}`
|
|
826
|
+
// )
|
|
827
|
+
const body = core.channel.reply.formatAgentEnvelope({
|
|
828
|
+
channel: channelId,
|
|
829
|
+
from: chatId,
|
|
830
|
+
timestamp: Date.now(),
|
|
831
|
+
envelope: envelopeOptions,
|
|
832
|
+
body: messageBody,
|
|
833
|
+
});
|
|
834
|
+
let combinedBody = body;
|
|
835
|
+
const historyKey = isGroup ? chatId : undefined;
|
|
836
|
+
if (isGroup && historyKey && chatHistories) {
|
|
837
|
+
combinedBody = createChannelHistoryWindow({
|
|
838
|
+
historyMap: chatHistories
|
|
839
|
+
}).buildPendingContext({
|
|
840
|
+
historyKey,
|
|
841
|
+
limit: historyLimit,
|
|
842
|
+
currentMessage: combinedBody,
|
|
843
|
+
formatEntry(entry) {
|
|
844
|
+
return core.channel.reply.formatAgentEnvelope({
|
|
845
|
+
channel: channelId,
|
|
846
|
+
// Preserve speaker identity in group history as well.
|
|
847
|
+
from: `${chatId}:${entry.sender}`,
|
|
848
|
+
timestamp: entry.timestamp,
|
|
849
|
+
body: entry.body,
|
|
850
|
+
envelope: envelopeOptions,
|
|
851
|
+
});
|
|
852
|
+
},
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
const inboundHistory = isGroup && historyKey && historyLimit > 0 && chatHistories
|
|
856
|
+
? (chatHistories.get(historyKey) ?? []).map((entry) => ({
|
|
857
|
+
sender: entry.sender,
|
|
858
|
+
body: entry.body,
|
|
859
|
+
timestamp: entry.timestamp,
|
|
860
|
+
}))
|
|
861
|
+
: undefined;
|
|
862
|
+
logger.info(`${channelId} account[${account.accountId}], agentId=${agentId},sessionKey=${sessionKey}, isEnqueueEvent=${isEnqueueEvent}`);
|
|
863
|
+
logger.info(`combined body:${combinedBody}\n`);
|
|
864
|
+
logger.info(`inbound History:${JSON.stringify(inboundHistory)}\n`);
|
|
865
|
+
// 构建标准消息上下文
|
|
866
|
+
return buildChannelInboundEventContext({
|
|
867
|
+
channel: channelId,
|
|
868
|
+
provider: channelId,
|
|
869
|
+
surface: channelId,
|
|
870
|
+
timestamp: Date.now(),
|
|
871
|
+
accountId: account.accountId,
|
|
872
|
+
messageId: msg.streamId,
|
|
873
|
+
from: from,
|
|
874
|
+
sender: {
|
|
875
|
+
id: fromUser.empId
|
|
876
|
+
},
|
|
877
|
+
conversation: {
|
|
878
|
+
kind: chatType,
|
|
879
|
+
id: chatId,
|
|
880
|
+
label: fromLabel + '_' + streamId //加上流ID要在session.message件事中获取
|
|
881
|
+
},
|
|
882
|
+
route: {
|
|
883
|
+
agentId: agentId, //route.agentId,
|
|
884
|
+
routeSessionKey: sessionKey //route.sessionKey
|
|
885
|
+
},
|
|
886
|
+
reply: {
|
|
887
|
+
to: to,
|
|
888
|
+
originatingTo: to,
|
|
889
|
+
replyToId: to
|
|
890
|
+
},
|
|
891
|
+
message: {
|
|
892
|
+
rawBody: messageBody,
|
|
893
|
+
body: combinedBody,
|
|
894
|
+
commandBody: combinedBody,
|
|
895
|
+
bodyForAgent: combinedBody,
|
|
896
|
+
inboundHistory: inboundHistory,
|
|
897
|
+
},
|
|
898
|
+
messageIdFull: msg.msgId,
|
|
899
|
+
access: {
|
|
900
|
+
commands: {
|
|
901
|
+
// 2026-09-06 适配 openclaw 2026.9.1:access.commands.authorized 由可选改为必填 boolean
|
|
902
|
+
authorized: commandAuthorized ?? false
|
|
903
|
+
}
|
|
904
|
+
},
|
|
905
|
+
command: {
|
|
906
|
+
kind: 'text-slash',
|
|
907
|
+
body: messageBody,
|
|
908
|
+
authorized: commandAuthorized
|
|
909
|
+
},
|
|
910
|
+
commandTurn: {
|
|
911
|
+
kind: "native",
|
|
912
|
+
source: "native",
|
|
913
|
+
authorized: commandAuthorized || false
|
|
914
|
+
},
|
|
915
|
+
media: mediaList,
|
|
916
|
+
supplemental: {
|
|
917
|
+
quote: {
|
|
918
|
+
body: quoteContent
|
|
919
|
+
}
|
|
920
|
+
},
|
|
921
|
+
contextVisibility: 'all',
|
|
922
|
+
//finalize: FinalizeInboundContextFn,
|
|
923
|
+
finalizeOptions: {
|
|
924
|
+
forceBodyForAgent: true,
|
|
925
|
+
forceChatType: true
|
|
926
|
+
// 2026-09-06 适配 openclaw 2026.9.1:forceConversationLabel 选项已移除,conversation.label 直接生效
|
|
927
|
+
}
|
|
928
|
+
//extra?: Record<string, unknown>;
|
|
929
|
+
});
|
|
930
|
+
}
|
|
931
|
+
/**
|
|
932
|
+
* 路由消息到核心处理流程并处理回复
|
|
933
|
+
*/
|
|
934
|
+
export async function routeAndDispatchMessage(ctx) {
|
|
935
|
+
const { ctxPayload, core, cfg, client, accountId, streamId, accStatus, commandAuthorized, chatOpts, logger } = ctx;
|
|
936
|
+
if (!commandAuthorized) {
|
|
937
|
+
await client.publishProcessingState({
|
|
938
|
+
streamId,
|
|
939
|
+
content: '[tag_status_running]',
|
|
940
|
+
status: 'read',
|
|
941
|
+
kind: 'in_process'
|
|
942
|
+
});
|
|
943
|
+
}
|
|
944
|
+
try {
|
|
945
|
+
const ctx = await ctxPayload;
|
|
946
|
+
const mediaFiles = [];
|
|
947
|
+
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
948
|
+
ctx,
|
|
949
|
+
cfg,
|
|
950
|
+
dispatcherOptions: {
|
|
951
|
+
onReplyStart: async () => {
|
|
952
|
+
await client.publishProcessingState({
|
|
953
|
+
streamId,
|
|
954
|
+
content: '[tag_agent_doing]',
|
|
955
|
+
status: 'read',
|
|
956
|
+
kind: 'block'
|
|
957
|
+
});
|
|
958
|
+
},
|
|
959
|
+
deliver: async (payload, info) => {
|
|
960
|
+
if (payload.isError) {
|
|
961
|
+
logger.error(`account[${accountId}], payload error!`);
|
|
962
|
+
}
|
|
963
|
+
//info.kind='block'
|
|
964
|
+
const contents = payload.text ? [payload.text] : [];
|
|
965
|
+
if (payload.mediaUrl || (payload.mediaUrls && payload.mediaUrls.length > 0)) {
|
|
966
|
+
//处理媒体文件
|
|
967
|
+
// 2026-09-01 修复:mediaUrl 与 mediaUrls 可能同时携带同一文件(语音场景),去重避免重复发送导致空语音
|
|
968
|
+
let files = [...new Set([...(payload.mediaUrl ? [payload.mediaUrl] : []), ...(payload.mediaUrls ?? [])])];
|
|
969
|
+
files.forEach(item => mediaFiles.push(item));
|
|
970
|
+
logger.info(`account[${accountId}], reply media file >>> ${files.join(',')}`);
|
|
971
|
+
}
|
|
972
|
+
if (contents.length === 0) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const content = contents.join(',');
|
|
976
|
+
accStatus.accumulatedPayload += content;
|
|
977
|
+
if (info.kind === 'tool') {
|
|
978
|
+
logger.info(`account[${accountId}] 正在调用工具.🔧${content}`);
|
|
979
|
+
await client.publishProcessingState({
|
|
980
|
+
streamId,
|
|
981
|
+
content: `[tag_status_call_tool]`,
|
|
982
|
+
status: 'read',
|
|
983
|
+
kind: 'block',
|
|
984
|
+
});
|
|
985
|
+
}
|
|
986
|
+
},
|
|
987
|
+
onSettled: async () => {
|
|
988
|
+
if ((accStatus.accumulatedPayload && accStatus.accumulatedPayload.length > 0) || mediaFiles.length > 0) {
|
|
989
|
+
//发送结束状态
|
|
990
|
+
await client.publishProcessingState({
|
|
991
|
+
streamId,
|
|
992
|
+
content: '[tag_status_completed]',
|
|
993
|
+
status: 'read',
|
|
994
|
+
kind: 'in_process'
|
|
995
|
+
});
|
|
996
|
+
//处理完成整体回复
|
|
997
|
+
await client.publishToServer({
|
|
998
|
+
streamId,
|
|
999
|
+
content: accStatus.accumulatedPayload,
|
|
1000
|
+
finish: true,
|
|
1001
|
+
kind: 'block',
|
|
1002
|
+
mediaFiles
|
|
1003
|
+
}, chatOpts);
|
|
1004
|
+
}
|
|
1005
|
+
},
|
|
1006
|
+
onError: async (err, info) => {
|
|
1007
|
+
const errorStr = String(err);
|
|
1008
|
+
await client.publishProcessingState({
|
|
1009
|
+
streamId,
|
|
1010
|
+
content: `[tag_status_error] ${errorStr}`,
|
|
1011
|
+
status: 'read',
|
|
1012
|
+
kind: 'in_process'
|
|
1013
|
+
});
|
|
1014
|
+
client.publishToServer({
|
|
1015
|
+
streamId,
|
|
1016
|
+
content: `[tag_error_pross]\n${errorStr}`,
|
|
1017
|
+
finish: info.kind === 'final',
|
|
1018
|
+
kind: 'block',
|
|
1019
|
+
mediaFiles
|
|
1020
|
+
}, chatOpts);
|
|
1021
|
+
},
|
|
1022
|
+
},
|
|
1023
|
+
replyOptions: {
|
|
1024
|
+
suppressDefaultToolProgressMessages: true,
|
|
1025
|
+
allowProgressCallbacksWhenSourceDeliverySuppressed: true,
|
|
1026
|
+
enableHeartbeatTool: true,
|
|
1027
|
+
onAgentRunStart: (runId) => {
|
|
1028
|
+
logger.info(`Agent run start ... runId = ${runId}`);
|
|
1029
|
+
client.publishProcessingState({
|
|
1030
|
+
streamId,
|
|
1031
|
+
content: '[tag_agent_doing]',
|
|
1032
|
+
status: 'read',
|
|
1033
|
+
kind: 'in_process',
|
|
1034
|
+
runId
|
|
1035
|
+
});
|
|
1036
|
+
},
|
|
1037
|
+
async onReplyStart() {
|
|
1038
|
+
await client.publishProcessingState({
|
|
1039
|
+
streamId,
|
|
1040
|
+
content: '[tag_agent_to_reply]',
|
|
1041
|
+
status: 'read',
|
|
1042
|
+
kind: 'in_process'
|
|
1043
|
+
});
|
|
1044
|
+
},
|
|
1045
|
+
onReasoningStream: async (payload) => {
|
|
1046
|
+
await client.publishProcessingState({
|
|
1047
|
+
streamId,
|
|
1048
|
+
content: `[tag_status_start_reasoning]${payload.text ?? ''}`,
|
|
1049
|
+
status: 'read',
|
|
1050
|
+
kind: 'reasoning'
|
|
1051
|
+
});
|
|
1052
|
+
},
|
|
1053
|
+
onReasoningEnd: async () => {
|
|
1054
|
+
await client.publishProcessingState({
|
|
1055
|
+
streamId,
|
|
1056
|
+
content: '[tag_status_end_reasoning]',
|
|
1057
|
+
status: 'read',
|
|
1058
|
+
kind: 'end_reasoning'
|
|
1059
|
+
});
|
|
1060
|
+
},
|
|
1061
|
+
onToolStart: async (payload) => {
|
|
1062
|
+
if (!payload.name) {
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
const messages = [
|
|
1066
|
+
`[tag_status_call_tool]`,
|
|
1067
|
+
`[tag_status_phase_${payload.phase}]`,
|
|
1068
|
+
payload.name ?? payload.toolCallId
|
|
1069
|
+
];
|
|
1070
|
+
await client.publishProcessingState({
|
|
1071
|
+
streamId,
|
|
1072
|
+
content: messages.join('\n'),
|
|
1073
|
+
status: 'read',
|
|
1074
|
+
kind: 'tool'
|
|
1075
|
+
});
|
|
1076
|
+
},
|
|
1077
|
+
onToolResult: async (payload) => {
|
|
1078
|
+
if (payload.isError) {
|
|
1079
|
+
await client.publishProcessingState({
|
|
1080
|
+
streamId,
|
|
1081
|
+
content: `[tag_status_error_tool]`,
|
|
1082
|
+
status: 'read',
|
|
1083
|
+
kind: 'tool',
|
|
1084
|
+
});
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
const content = payload.text || payload.spokenText || payload.btw?.question || '';
|
|
1088
|
+
if (content.length > 0) {
|
|
1089
|
+
await client.publishProcessingState({
|
|
1090
|
+
streamId,
|
|
1091
|
+
content,
|
|
1092
|
+
status: 'read',
|
|
1093
|
+
kind: 'plan',
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
},
|
|
1097
|
+
async onItemEvent(payload) {
|
|
1098
|
+
const kind = payload.kind;
|
|
1099
|
+
if (!kind) {
|
|
1100
|
+
return;
|
|
1101
|
+
}
|
|
1102
|
+
const content = [];
|
|
1103
|
+
if (payload.name) {
|
|
1104
|
+
content.push(payload.name);
|
|
1105
|
+
}
|
|
1106
|
+
if (payload.title) {
|
|
1107
|
+
content.push(payload.title);
|
|
1108
|
+
}
|
|
1109
|
+
if (payload.progressText) {
|
|
1110
|
+
content.push(payload.progressText);
|
|
1111
|
+
}
|
|
1112
|
+
if (payload.summary) {
|
|
1113
|
+
content.push(payload.summary);
|
|
1114
|
+
}
|
|
1115
|
+
const messages = [
|
|
1116
|
+
`[tag_status_call_${kind}]`,
|
|
1117
|
+
`[tag_status_phase_${payload.phase}]`,
|
|
1118
|
+
`[tag_status_${payload.status}]`,
|
|
1119
|
+
...content
|
|
1120
|
+
];
|
|
1121
|
+
await client.publishProcessingState({
|
|
1122
|
+
streamId,
|
|
1123
|
+
content: messages.join('>'),
|
|
1124
|
+
status: 'read',
|
|
1125
|
+
kind: kind
|
|
1126
|
+
});
|
|
1127
|
+
logger.info(`onItemEvent >>> kind=${payload.kind}, phase=${payload.phase}, status =${payload.status}, content=${content.join('>')}`);
|
|
1128
|
+
},
|
|
1129
|
+
onCommandOutput: async (payload) => {
|
|
1130
|
+
if (!payload.phase) {
|
|
1131
|
+
return;
|
|
1132
|
+
}
|
|
1133
|
+
const messages = [
|
|
1134
|
+
`[tag_status_call_command]`,
|
|
1135
|
+
`[tag_status_phase_${payload.phase}]`,
|
|
1136
|
+
`[tag_status_${payload.status}]`,
|
|
1137
|
+
payload.name ?? '',
|
|
1138
|
+
payload.output ?? ''
|
|
1139
|
+
];
|
|
1140
|
+
await client.publishProcessingState({
|
|
1141
|
+
streamId,
|
|
1142
|
+
content: messages.join('>'),
|
|
1143
|
+
status: 'read',
|
|
1144
|
+
kind: 'command'
|
|
1145
|
+
});
|
|
1146
|
+
},
|
|
1147
|
+
onPatchSummary: async (payload) => {
|
|
1148
|
+
const strs = [];
|
|
1149
|
+
if (payload.title) {
|
|
1150
|
+
strs.push(payload.title);
|
|
1151
|
+
}
|
|
1152
|
+
if (payload.summary) {
|
|
1153
|
+
strs.push(payload.summary);
|
|
1154
|
+
}
|
|
1155
|
+
if (strs.length > 0)
|
|
1156
|
+
logger.info(`onPatchSummary 输出 >>> ${strs.join('')}`);
|
|
1157
|
+
await client.publishProcessingState({
|
|
1158
|
+
streamId,
|
|
1159
|
+
content: strs.join(''),
|
|
1160
|
+
status: 'read',
|
|
1161
|
+
kind: 'patch'
|
|
1162
|
+
});
|
|
1163
|
+
},
|
|
1164
|
+
async onPlanUpdate(payload) {
|
|
1165
|
+
const strs = [];
|
|
1166
|
+
if (payload.explanation) {
|
|
1167
|
+
strs.push(payload.explanation);
|
|
1168
|
+
}
|
|
1169
|
+
if (payload.steps) {
|
|
1170
|
+
strs.push('\n\n');
|
|
1171
|
+
payload.steps.forEach(str => strs.push(`- ${str}`));
|
|
1172
|
+
}
|
|
1173
|
+
if (strs.length > 0) {
|
|
1174
|
+
logger.info(`onPlanUpdate 输出 >>> ${strs.join('')}`);
|
|
1175
|
+
await client.publishProcessingState({
|
|
1176
|
+
streamId,
|
|
1177
|
+
content: strs.join(''),
|
|
1178
|
+
status: 'read',
|
|
1179
|
+
kind: 'plan'
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
},
|
|
1183
|
+
onCompactionStart: async () => {
|
|
1184
|
+
await client.publishProcessingState({
|
|
1185
|
+
streamId,
|
|
1186
|
+
content: '[tag_status_compaction_start]',
|
|
1187
|
+
status: 'read',
|
|
1188
|
+
kind: 'block'
|
|
1189
|
+
});
|
|
1190
|
+
},
|
|
1191
|
+
onCompactionEnd: async () => {
|
|
1192
|
+
await client.publishProcessingState({
|
|
1193
|
+
streamId,
|
|
1194
|
+
content: '[tag_status_compaction_end]',
|
|
1195
|
+
status: 'read',
|
|
1196
|
+
kind: 'block'
|
|
1197
|
+
});
|
|
1198
|
+
},
|
|
1199
|
+
}
|
|
1200
|
+
});
|
|
1201
|
+
}
|
|
1202
|
+
catch (err) {
|
|
1203
|
+
logger.error(`account[${accountId}] Failed to process message: ${String(err)}`);
|
|
1204
|
+
client.publishToServer({
|
|
1205
|
+
streamId,
|
|
1206
|
+
content: `[tag_error_reply_fail]:${String(err)}`,
|
|
1207
|
+
finish: true,
|
|
1208
|
+
kind: 'block'
|
|
1209
|
+
}, chatOpts);
|
|
1210
|
+
}
|
|
1211
|
+
}
|