@qihoo/tuitui-openclaw-channel 1.0.22 → 1.0.23
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/deduplicator.ts +121 -0
- package/src/inbound.ts +17 -7
- package/src/outbound.ts +8 -5
package/package.json
CHANGED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export class StringDeduplicator {
|
|
2
|
+
private items = new Map<string, number>(); // 存储元素和其首次插入时间(秒级时间戳)
|
|
3
|
+
private readonly maxSize: number;
|
|
4
|
+
private readonly expireTime: number | null; // 过期时间(秒),null 表示永不过期
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* 字符串去重器
|
|
8
|
+
* @param maxSize - 最大缓存元素数量,默认1000
|
|
9
|
+
* @param expireTime - 过期时间(秒),默认null(永不过期)。例如:300表示5分钟
|
|
10
|
+
*/
|
|
11
|
+
constructor(maxSize: number = 1000, expireTime: number | null = null) {
|
|
12
|
+
if (maxSize <= 0) {
|
|
13
|
+
throw new Error(`[StringDeduplicator] maxSize must be positive, got ${maxSize}`);
|
|
14
|
+
}
|
|
15
|
+
if (expireTime !== null && expireTime <= 0) {
|
|
16
|
+
throw new Error(`[StringDeduplicator] expireTime must be positive or null, got ${expireTime}`);
|
|
17
|
+
}
|
|
18
|
+
this.maxSize = maxSize;
|
|
19
|
+
this.expireTime = expireTime;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 检查并记录字符串
|
|
24
|
+
* @returns true 表示在有效时间内首次出现(可处理),false 表示重复或已过期
|
|
25
|
+
*/
|
|
26
|
+
checkAndRecord(item: string): boolean {
|
|
27
|
+
if (item === undefined || item === null) {
|
|
28
|
+
throw new Error('[StringDeduplicator] item cannot be null or undefined');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const now = Date.now() / 1000; // 转换为秒级时间戳
|
|
32
|
+
|
|
33
|
+
// 检查是否已存在
|
|
34
|
+
const firstSeenTime = this.items.get(item);
|
|
35
|
+
if (firstSeenTime !== undefined) {
|
|
36
|
+
// 如果设置了过期时间,检查是否过期
|
|
37
|
+
if (this.expireTime !== null && now - firstSeenTime >= this.expireTime) {
|
|
38
|
+
// 已过期,移除旧记录,重新添加
|
|
39
|
+
this.items.delete(item);
|
|
40
|
+
} else if (this.expireTime === null || now - firstSeenTime < this.expireTime) {
|
|
41
|
+
// 未过期(或永不过期模式),重复
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// 添加新记录
|
|
47
|
+
this.items.set(item, now);
|
|
48
|
+
this.enforceSizeLimit();
|
|
49
|
+
this.cleanupExpired();
|
|
50
|
+
return true; // 首次出现或已过期后重新出现
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 清理过期的条目
|
|
55
|
+
*/
|
|
56
|
+
private cleanupExpired(): void {
|
|
57
|
+
if (this.expireTime === null) return;
|
|
58
|
+
|
|
59
|
+
const now = Date.now() / 1000;
|
|
60
|
+
for (const [item, timestamp] of this.items.entries()) {
|
|
61
|
+
if (now - timestamp >= this.expireTime) {
|
|
62
|
+
this.items.delete(item);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* 确保不超过最大大小,删除最老的条目
|
|
69
|
+
*/
|
|
70
|
+
private enforceSizeLimit(): void {
|
|
71
|
+
if (this.items.size <= this.maxSize) return;
|
|
72
|
+
|
|
73
|
+
// 因为每次 checkAndRecord 最多插入1个元素,所以只需删除1个最老的
|
|
74
|
+
const iterator = this.items.entries();
|
|
75
|
+
const oldestEntry = iterator.next().value;
|
|
76
|
+
if (oldestEntry) {
|
|
77
|
+
const [oldestItem] = oldestEntry;
|
|
78
|
+
this.items.delete(oldestItem);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// 可选:手动清理
|
|
83
|
+
clear(): void {
|
|
84
|
+
this.items.clear();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// 可选:检查是否存在(不记录)
|
|
88
|
+
has(item: string): boolean {
|
|
89
|
+
const firstSeenTime = this.items.get(item);
|
|
90
|
+
if (firstSeenTime === undefined) return false;
|
|
91
|
+
// 如果设置了过期时间,检查是否过期
|
|
92
|
+
if (this.expireTime !== null) {
|
|
93
|
+
return (Date.now() / 1000) - firstSeenTime < this.expireTime;
|
|
94
|
+
}
|
|
95
|
+
return true;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// 可选:获取当前大小
|
|
99
|
+
size(): number {
|
|
100
|
+
// 懒清理:每10次size调用执行一次完整清理
|
|
101
|
+
if (Math.random() < 0.1) {
|
|
102
|
+
this.cleanupExpired();
|
|
103
|
+
}
|
|
104
|
+
return this.items.size;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// 可选:删除指定项
|
|
108
|
+
delete(item: string): boolean {
|
|
109
|
+
return this.items.delete(item);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 可选:获取过期时间(秒)
|
|
113
|
+
getExpireTime(): number | null {
|
|
114
|
+
return this.expireTime;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 可选:获取最大大小
|
|
118
|
+
getMaxSize(): number {
|
|
119
|
+
return this.maxSize;
|
|
120
|
+
}
|
|
121
|
+
}
|
package/src/inbound.ts
CHANGED
|
@@ -20,13 +20,15 @@ import {
|
|
|
20
20
|
sendMediaMsg,
|
|
21
21
|
teamsBuildChatId,
|
|
22
22
|
teamsParseChatId,
|
|
23
|
+
get_announcement,
|
|
23
24
|
} from "./outbound";
|
|
24
25
|
import { parseChatMessageBody } from './inbound_body_parse';
|
|
25
26
|
import { parseAllowFroms } from './utils';
|
|
26
|
-
import {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
27
|
+
import { addUnmentionedHistory, popUnmentionedHistories } from "./histories";
|
|
28
|
+
import { StringDeduplicator } from "./deduplicator"
|
|
29
|
+
|
|
30
|
+
// 会话上下文注入排重
|
|
31
|
+
let _session_ctx_injected = new StringDeduplicator(1000, 3600);
|
|
30
32
|
|
|
31
33
|
/** 子函数共享的可变上下文,子函数直接修改字段,外层读取结果 */
|
|
32
34
|
interface ChatPayload {
|
|
@@ -352,9 +354,17 @@ async function parse_teams_post(payload: ChatPayload, msgData: any, account: Inb
|
|
|
352
354
|
}
|
|
353
355
|
}
|
|
354
356
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
357
|
+
if(_session_ctx_injected.checkAndRecord(accountId + "_" + payload.chatId)) {
|
|
358
|
+
// 注入上下文
|
|
359
|
+
// 解决机器人不知道自己是谁的问题
|
|
360
|
+
if(payload.botName) {
|
|
361
|
+
payload.text += `\n[备忘]\n你在当前session中的名字叫: ${payload.botName}\n 如果有人@这个名字,就是在命令你`;
|
|
362
|
+
}
|
|
363
|
+
// 公告
|
|
364
|
+
const annnouncement = await get_announcement(account, channel_id, false);
|
|
365
|
+
if(annnouncement) {
|
|
366
|
+
payload.text += `\n[当前公告内容如下--有需要时可参考]\n${annnouncement}`;
|
|
367
|
+
}
|
|
358
368
|
}
|
|
359
369
|
}
|
|
360
370
|
|
package/src/outbound.ts
CHANGED
|
@@ -591,15 +591,18 @@ function parseSessionKey(str: string): string {
|
|
|
591
591
|
}
|
|
592
592
|
|
|
593
593
|
// TODO: 支持群公告
|
|
594
|
-
export async function get_announcement(account: any,
|
|
595
|
-
|
|
596
|
-
if(
|
|
597
|
-
|
|
594
|
+
export async function get_announcement(account: any, id: any, id_is_session: boolean = true): Promise<any> {
|
|
595
|
+
let channel_id = id;
|
|
596
|
+
if(id_is_session) {
|
|
597
|
+
channel_id = parseSessionKey(id);
|
|
598
|
+
if(!channel_id) {
|
|
599
|
+
return "";
|
|
600
|
+
}
|
|
598
601
|
}
|
|
599
602
|
|
|
600
603
|
const payload = {channel_id: channel_id};
|
|
601
604
|
const body = await tuituiRobotApi(account, '/teams/channel/info', payload);
|
|
602
605
|
//console.log("info", data);
|
|
603
606
|
const announcement = body?.datas?.info?.announcement;
|
|
604
|
-
return
|
|
607
|
+
return announcement;
|
|
605
608
|
}
|