@qihoo/tuitui-openclaw-channel 1.0.14 → 1.0.15
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/package.json +1 -1
- package/src/confs.ts +1 -1
- package/src/histories.ts +69 -0
- package/src/inbound.ts +32 -5
- package/src/outbound.ts +1 -0
package/package.json
CHANGED
package/src/confs.ts
CHANGED
package/src/histories.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import {
|
|
2
|
+
DEFAULT_GROUP_HISTORY_LIMIT,
|
|
3
|
+
recordPendingHistoryEntryIfEnabled,
|
|
4
|
+
type HistoryEntry,
|
|
5
|
+
} from "openclaw/plugin-sdk/reply-history";
|
|
6
|
+
|
|
7
|
+
// 每个 accountId 独立的 group history map,防止多账户冲突
|
|
8
|
+
const accountGroupHistories = new Map<string, Map<string, HistoryEntry[]>>();
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 记录没有被@,被忽略的群消息到对应 account 的 pending history 中
|
|
12
|
+
*/
|
|
13
|
+
export async function addUnmentionedHistory(apiRuntime, accountId: string, chatId: string, tuituiUserName: any, tuituiAccount: string, text: any, timestamp :any) {
|
|
14
|
+
const cfg = await apiRuntime.config.loadConfig();
|
|
15
|
+
const senderDesc = tuituiUserName ? `${tuituiUserName} (${tuituiAccount})` : tuituiAccount;
|
|
16
|
+
|
|
17
|
+
const entry: HistoryEntry = {
|
|
18
|
+
sender: senderDesc,
|
|
19
|
+
body: text,
|
|
20
|
+
timestamp: timestamp ? Number(timestamp) : Date.now(),
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// 为每个 accountId 维护独立的 group history map
|
|
24
|
+
let groupHistories = accountGroupHistories.get(accountId);
|
|
25
|
+
if (!groupHistories) {
|
|
26
|
+
groupHistories = new Map<string, HistoryEntry[]>();
|
|
27
|
+
accountGroupHistories.set(accountId, groupHistories);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const historyLimit = resolveHistoryLimit(cfg);
|
|
31
|
+
|
|
32
|
+
// 这个API内部没啥神秘的,只是帮你维护map变量。openclaw本身不持有这些数据。
|
|
33
|
+
recordPendingHistoryEntryIfEnabled({
|
|
34
|
+
historyMap: groupHistories,
|
|
35
|
+
historyKey: chatId,
|
|
36
|
+
limit: historyLimit,
|
|
37
|
+
entry: entry,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* 获取指定 account 和群组的 pending history,格式化为 ctx.InboundHistory 所需的格式
|
|
43
|
+
*/
|
|
44
|
+
export function popUnmentionedHistories(accountId: string, chatId: string): {sender: string, body: string, timestamp?: number}[] {
|
|
45
|
+
const groupHistories = accountGroupHistories.get(accountId);
|
|
46
|
+
if (!groupHistories) {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
const his = groupHistories.get(chatId);
|
|
50
|
+
if (!his) {
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
groupHistories.delete(chatId);
|
|
55
|
+
|
|
56
|
+
return his.map((entry) => ({
|
|
57
|
+
sender: entry.sender,
|
|
58
|
+
body: entry.body,
|
|
59
|
+
timestamp: entry.timestamp,
|
|
60
|
+
}));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 从配置中解析 history limit
|
|
65
|
+
* dashboard->messages->groupChat->historyLimit
|
|
66
|
+
*/
|
|
67
|
+
export function resolveHistoryLimit(cfg: any): number {
|
|
68
|
+
return cfg.messages?.groupChat?.historyLimit ?? DEFAULT_GROUP_HISTORY_LIMIT;
|
|
69
|
+
}
|
package/src/inbound.ts
CHANGED
|
@@ -23,6 +23,10 @@ import {
|
|
|
23
23
|
teamsParseChatId,
|
|
24
24
|
} from "./outbound";
|
|
25
25
|
import { parseAllowFroms } from './utils';
|
|
26
|
+
import {
|
|
27
|
+
addUnmentionedHistory,
|
|
28
|
+
popUnmentionedHistories,
|
|
29
|
+
} from "./histories";
|
|
26
30
|
|
|
27
31
|
/** 子函数共享的可变上下文,子函数直接修改字段,外层读取结果 */
|
|
28
32
|
interface ChatPayload {
|
|
@@ -30,12 +34,14 @@ interface ChatPayload {
|
|
|
30
34
|
chatId: string | undefined;
|
|
31
35
|
text: string | undefined;
|
|
32
36
|
groupName: string | undefined;
|
|
37
|
+
channelName: string | undefined;
|
|
33
38
|
mediaUrls: string[] | undefined;
|
|
34
39
|
replyToId: string | undefined; // 引用ID
|
|
35
40
|
msgId?: string; // 消息ID
|
|
36
41
|
tuituiAccount: string;
|
|
37
42
|
tuituiUid: string | undefined;
|
|
38
43
|
tuituiUserName: string | undefined;
|
|
44
|
+
timestamp: Number | undefined;
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
export interface InboundAccount {
|
|
@@ -227,6 +233,7 @@ const parseAndVerifyPayload: Record<string, Function> = {
|
|
|
227
233
|
payload.chatId = chatId;
|
|
228
234
|
payload.groupName = msgData.group_name;
|
|
229
235
|
payload.msgId = msgData.msgid;
|
|
236
|
+
payload.text = buildMessageBody(msgData);
|
|
230
237
|
log?.debug?.(
|
|
231
238
|
`[${CHANNEL_ID}] AccountId: ${accountId}, inbound group_chat:
|
|
232
239
|
tuituiAccount=${payload.tuituiAccount},
|
|
@@ -249,7 +256,8 @@ const parseAndVerifyPayload: Record<string, Function> = {
|
|
|
249
256
|
}
|
|
250
257
|
|
|
251
258
|
if (!msgData.at_me && account.requireMention) {
|
|
252
|
-
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore group message (not mentioned)`);
|
|
259
|
+
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore group message (not mentioned), add to history key: ${chatId}`);
|
|
260
|
+
await addUnmentionedHistory(apiRuntime, accountId, chatId, payload.tuituiUserName, payload.tuituiAccount, payload.text, payload.timestamp);
|
|
253
261
|
return false;
|
|
254
262
|
}
|
|
255
263
|
|
|
@@ -268,19 +276,20 @@ const parseAndVerifyPayload: Record<string, Function> = {
|
|
|
268
276
|
return false;
|
|
269
277
|
}
|
|
270
278
|
|
|
271
|
-
|
|
279
|
+
|
|
272
280
|
|
|
273
281
|
return true;
|
|
274
282
|
},
|
|
275
283
|
teams_post_create: async (payload: ChatPayload, msgData: any, account: InboundAccount, apiRuntime: any, log: any): Promise<boolean> => {
|
|
276
284
|
const { accountId, groupPolicy, groupAllowFrom } = account;
|
|
277
285
|
payload.chatType = CHAT_TYPE_CHANNEL;
|
|
278
|
-
const { team_id, channel_id, parent_id, post_id, content } = msgData;
|
|
286
|
+
const { team_id, channel_id, parent_id, post_id, content, team_name, channel_name } = msgData;
|
|
279
287
|
const thread_id = (parent_id && parent_id != "0")?parent_id: post_id;
|
|
280
288
|
const chatId = teamsBuildChatId(team_id, channel_id, thread_id);
|
|
281
289
|
payload.chatId = chatId;
|
|
282
290
|
payload.msgId = post_id;
|
|
283
291
|
payload.text = content;
|
|
292
|
+
payload.channelName = channel_name;
|
|
284
293
|
payload.replyToId = "";
|
|
285
294
|
log?.debug?.(
|
|
286
295
|
`[${CHANNEL_ID}] AccountId: ${accountId}, inbound teams:
|
|
@@ -305,7 +314,8 @@ const parseAndVerifyPayload: Record<string, Function> = {
|
|
|
305
314
|
}
|
|
306
315
|
|
|
307
316
|
if (!msgData.at_me && account.requireMention) {
|
|
308
|
-
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned)`);
|
|
317
|
+
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, ignore teams post (not mentioned), add to history key: ${chatId}`);
|
|
318
|
+
await addUnmentionedHistory(apiRuntime, accountId, chatId, payload.tuituiUserName, payload.tuituiAccount, payload.text, payload.timestamp);
|
|
309
319
|
return false;
|
|
310
320
|
}
|
|
311
321
|
|
|
@@ -356,11 +366,13 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
|
|
|
356
366
|
chatId: undefined,
|
|
357
367
|
text: undefined,
|
|
358
368
|
groupName: undefined,
|
|
369
|
+
channelName: undefined,
|
|
359
370
|
mediaUrls: getMediaUrls(msgData),
|
|
360
371
|
replyToId: ref?.is_me && ref?.msgid ? ref.msgid : undefined,
|
|
361
372
|
tuituiAccount: msg.user_account || "",
|
|
362
373
|
tuituiUid: msg.uid || "",
|
|
363
374
|
tuituiUserName: msg.user_name || "",
|
|
375
|
+
timestamp: msg.timestamp ? parseInt(msg.timestamp, 10)*1000 : undefined
|
|
364
376
|
};
|
|
365
377
|
const wsEvent = json.body.event;
|
|
366
378
|
// 按event类型,标准化并校验基础数据
|
|
@@ -388,6 +400,14 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
|
|
|
388
400
|
const sessionKey = getSessionKey(cfg, payload, account, apiRuntime);
|
|
389
401
|
log?.info?.(`[${CHANNEL_ID}] AccountId: ${accountId}, dispatching to agent session=${sessionKey}, chatType: ${payload.chatType}, chatId ${payload.chatId}, routeSenderFrom: ${routeSenderFrom}`);
|
|
390
402
|
|
|
403
|
+
if(payload.text?.startsWith("/new") || payload.text?.endsWith("/new")) {
|
|
404
|
+
// 清理agent上下文时,同时清空历史
|
|
405
|
+
popUnmentionedHistories(accountId, payload.chatId);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
// 字段意义 参考 openclaw 代码
|
|
409
|
+
// src\auto-reply\reply\inbound-meta.ts
|
|
410
|
+
// buildInboundUserContextPrefix()
|
|
391
411
|
const ctx: any = {
|
|
392
412
|
Body: payload.text! || ' ',
|
|
393
413
|
From: String(routeSenderFrom),
|
|
@@ -401,13 +421,20 @@ export async function handleInboundMessage({ json, account, apiRuntime, log }: I
|
|
|
401
421
|
Surface: CHANNEL_ID,
|
|
402
422
|
Provider: CHANNEL_ID,
|
|
403
423
|
SenderName: payload.tuituiUserName,
|
|
404
|
-
|
|
424
|
+
SenderId: payload.tuituiAccount,
|
|
425
|
+
SenderUsername: payload.tuituiAccount,
|
|
405
426
|
UserAccount: payload.tuituiAccount,
|
|
427
|
+
Timestamp: payload.timestamp,
|
|
406
428
|
CommandAuthorized: true, // 允许 /new 等内置命令
|
|
407
429
|
};
|
|
408
430
|
if (payload.chatType == CHAT_TYPE_GROUP && payload.chatId) {
|
|
409
431
|
ctx.GroupSubject = payload.groupName;
|
|
410
432
|
ctx.GroupId = payload.chatId;
|
|
433
|
+
ctx.InboundHistory = popUnmentionedHistories(accountId, payload.chatId);
|
|
434
|
+
}
|
|
435
|
+
if (payload.chatType == CHAT_TYPE_CHANNEL) {
|
|
436
|
+
ctx.GroupChannel = payload.channelName;
|
|
437
|
+
ctx.InboundHistory = popUnmentionedHistories(accountId, payload.chatId);
|
|
411
438
|
}
|
|
412
439
|
if (payload.mediaUrls?.length) ctx.MediaUrls = payload.mediaUrls;
|
|
413
440
|
if (payload.replyToId) ctx.ReplyToId = payload.replyToId;
|
package/src/outbound.ts
CHANGED
|
@@ -491,6 +491,7 @@ export async function sendMediaMsg(
|
|
|
491
491
|
atList?: string[],
|
|
492
492
|
): Promise<void> {
|
|
493
493
|
if (!chatId) return console.error(`[${CHANNEL_ID}] sendMediaMsg Error ${auditCtx}: Missing "target"`);
|
|
494
|
+
console.log(`[${CHANNEL_ID}] sendMediaMsg ${chatType} ${chatId} ${auditCtx} uploading`);
|
|
494
495
|
// Check if mediaUrl looks like an image
|
|
495
496
|
const isImage = /^data:image\//i.test(mediaUrl) || /\.(jpg|jpeg|png|gif)(?:$|[?#])/i.test(mediaUrl);
|
|
496
497
|
const mediaType = isImage ? 'image' : 'file';
|