koishi-plugin-group-memory 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/package.json +42 -0
- package/src/index.js +190 -0
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "koishi-plugin-group-memory",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "智能群聊记忆中间件:自动记录上下文,在@或回复时注入前情提要,支持时间窗口过滤与内容清洗",
|
|
5
|
+
"main": "./src/index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"koishi",
|
|
11
|
+
"plugin",
|
|
12
|
+
"memory",
|
|
13
|
+
"context",
|
|
14
|
+
"group-chat",
|
|
15
|
+
"ai",
|
|
16
|
+
"middleware",
|
|
17
|
+
"deepseek",
|
|
18
|
+
"openai"
|
|
19
|
+
],
|
|
20
|
+
"author": "YourName",
|
|
21
|
+
"license": "MIT",
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": ""
|
|
25
|
+
},
|
|
26
|
+
"koishi": {
|
|
27
|
+
"description": {
|
|
28
|
+
"en": "Smart group chat memory middleware. Automatically records context and injects summaries when @mentioned or replied. Supports time-window filtering, content cleaning (mentions/images), and custom templates. Perfect for making AI bots aware of ongoing conversations.",
|
|
29
|
+
"zh": "智能群聊记忆中间件。自动记录群内闲聊上下文,当用户 @机器人 或回复机器人时,自动将“前情提要”注入给 AI 插件。支持按时间/条数过滤记忆、自动清洗 @符号和图片、自定义提示词模板。是让 AI 机器人拥有“群聊感知力”的必备插件。"
|
|
30
|
+
},
|
|
31
|
+
"service": {
|
|
32
|
+
"required": [],
|
|
33
|
+
"optional": []
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
"peerDependencies": {
|
|
37
|
+
"koishi": "^4.16.0"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=18.0.0"
|
|
41
|
+
}
|
|
42
|
+
}
|
package/src/index.js
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
const { Context } = require('koishi');
|
|
2
|
+
|
|
3
|
+
exports.name = 'group-memory';
|
|
4
|
+
exports.usage = '自动记录群聊上下文,并在用户@机器人或回复机器人时,将前情提要注入给 AI 插件';
|
|
5
|
+
|
|
6
|
+
// 配置架构
|
|
7
|
+
exports.Config = (ctx) => ({
|
|
8
|
+
// --- 记忆设置 ---
|
|
9
|
+
maxContext: 20, // 每个群保留最近多少条消息
|
|
10
|
+
timeWindow: 3600000, // (可选) 只保留最近 N 毫秒内的消息 (默认 1 小时),0 表示不限制时间只限制条数
|
|
11
|
+
|
|
12
|
+
// --- 触发条件 ---
|
|
13
|
+
triggerOnAt: true, // 当消息中包含 @机器人 时触发
|
|
14
|
+
triggerOnReply: true, // 当消息是回复机器人的消息时触发
|
|
15
|
+
|
|
16
|
+
// --- 适配设置 ---
|
|
17
|
+
// 目标 AI 插件的名称列表。当消息命中这些插件的命令或逻辑时,才注入上下文。
|
|
18
|
+
// 留空则对所有消息生效(只要满足 @ 或回复条件)。
|
|
19
|
+
// 对于 deepseek-chat 插件,通常不需要填,因为它是通过中间件拦截所有内容的。
|
|
20
|
+
// 但如果你的 AI 插件有特定的命令前缀,可以在这里辅助判断。
|
|
21
|
+
targetPlugins: [],
|
|
22
|
+
|
|
23
|
+
// --- 内容处理 ---
|
|
24
|
+
cleanMentions: true, // 发送给 AI 前,是否移除消息中的 @ 符号 (避免 AI 困惑)
|
|
25
|
+
cleanImages: true, // 是否移除图片/媒体占位符 (如 [图片])
|
|
26
|
+
prefixTemplate: (history) => {
|
|
27
|
+
// 自定义前情提要的模板函数
|
|
28
|
+
return "【群聊前情提要】\n" +
|
|
29
|
+
history.map(msg => `${msg.timeStr} ${msg.user}: ${msg.content}`).join('\n') +
|
|
30
|
+
"\n---\n请结合以上上下文回答以下问题:";
|
|
31
|
+
},
|
|
32
|
+
|
|
33
|
+
// --- 调试 ---
|
|
34
|
+
verbose: false // 是否在控制台打印注入的上下文
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
exports.apply = (ctx, config) => {
|
|
38
|
+
// 内存存储:{ groupId: [ { user: string, content: string, time: number, timeStr: string }, ... ] }
|
|
39
|
+
const groupChats = new Map();
|
|
40
|
+
|
|
41
|
+
// 工具函数:格式化时间
|
|
42
|
+
const formatTime = (timestamp) => {
|
|
43
|
+
const date = new Date(timestamp);
|
|
44
|
+
return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// 工具函数:清洗消息内容
|
|
48
|
+
const cleanContent = (content, session) => {
|
|
49
|
+
let text = content;
|
|
50
|
+
|
|
51
|
+
// 1. 移除 @ 提及 (特别是 @机器人)
|
|
52
|
+
if (config.cleanMentions) {
|
|
53
|
+
// 移除 koishi 标准的 @ 元素标记 (如果是 element 对象)
|
|
54
|
+
// 这里主要处理纯文本中的 @
|
|
55
|
+
text = text.replace(new RegExp(`@${session.selfId}`, 'g'), '');
|
|
56
|
+
text = text.replace(/@\S+/g, (match) => {
|
|
57
|
+
// 可以选择保留其他人名,或者全部移除,这里选择移除 @ 符号但保留名字,或者直接移除整段
|
|
58
|
+
// 为了 AI 理解,通常移除 @ 符号即可
|
|
59
|
+
return match.replace('@', '');
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. 移除图片/媒体占位符 (根据具体协议可能不同,这里做通用处理)
|
|
64
|
+
if (config.cleanImages) {
|
|
65
|
+
text = text.replace(/\[图片\]/g, '[图片已省略]');
|
|
66
|
+
text = text.replace(/\[表情.*?\]/g, '');
|
|
67
|
+
// 移除 XML 或 JSON 形式的媒体元素 (常见于 QQ/OneBot)
|
|
68
|
+
text = text.replace(/\[CQ:.*?\]/g, '');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return text.trim();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
// --- 中间件 1: 记录群聊历史 ---
|
|
75
|
+
ctx.middleware(async (session, next) => {
|
|
76
|
+
// 只在群聊中生效
|
|
77
|
+
if (!session.groupId) return next();
|
|
78
|
+
|
|
79
|
+
// 忽略机器人自己的消息
|
|
80
|
+
if (session.selfId === session.userId) return next();
|
|
81
|
+
|
|
82
|
+
// 忽略指令消息 (以 . / \ 开头),避免把命令本身记入上下文干扰 AI
|
|
83
|
+
const trimmed = session.content?.trim() || '';
|
|
84
|
+
if (/^[./\\]/.test(trimmed)) return next();
|
|
85
|
+
|
|
86
|
+
// 忽略空消息
|
|
87
|
+
if (!trimmed) return next();
|
|
88
|
+
|
|
89
|
+
const groupId = session.groupId;
|
|
90
|
+
if (!groupChats.has(groupId)) {
|
|
91
|
+
groupChats.set(groupId, []);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const history = groupChats.get(groupId);
|
|
95
|
+
const now = Date.now();
|
|
96
|
+
|
|
97
|
+
// 记录消息
|
|
98
|
+
const cleanText = cleanContent(session.content, session);
|
|
99
|
+
|
|
100
|
+
history.push({
|
|
101
|
+
user: session.author?.nickname || session.userId,
|
|
102
|
+
content: cleanText,
|
|
103
|
+
time: now,
|
|
104
|
+
timeStr: formatTime(now)
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
// 1. 限制条数
|
|
108
|
+
while (history.length > config.maxContext) {
|
|
109
|
+
history.shift();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// 2. 限制时间窗口 (如果配置了)
|
|
113
|
+
if (config.timeWindow > 0) {
|
|
114
|
+
const cutoff = now - config.timeWindow;
|
|
115
|
+
while (history.length > 0 && history[0].time < cutoff) {
|
|
116
|
+
history.shift();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return next();
|
|
121
|
+
}, true); // priority: true (确保尽早记录)
|
|
122
|
+
|
|
123
|
+
// --- 中间件 2: 检测触发并注入上下文 ---
|
|
124
|
+
// 优先级设为低 (100),确保在其他插件处理之前完成注入,但又不至于太早导致拿不到解析结果
|
|
125
|
+
ctx.middleware(async (session, next) => {
|
|
126
|
+
if (!session.groupId) return next();
|
|
127
|
+
|
|
128
|
+
// 检查触发条件
|
|
129
|
+
let shouldInject = false;
|
|
130
|
+
|
|
131
|
+
// 1. 检查 @ 机器人
|
|
132
|
+
if (config.triggerOnAt) {
|
|
133
|
+
// Koishi 标准解析方式
|
|
134
|
+
const isAtBot = session.parsed?.bots?.includes(session.selfId) ||
|
|
135
|
+
(session.content && new RegExp(`<@${session.selfId}>|@${session.selfId}`).test(session.content));
|
|
136
|
+
if (isAtBot) shouldInject = true;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2. 检查回复机器人
|
|
140
|
+
if (!shouldInject && config.triggerOnReply && session.quote) {
|
|
141
|
+
if (session.quote.userId === session.selfId) {
|
|
142
|
+
shouldInject = true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (!shouldInject) return next();
|
|
147
|
+
|
|
148
|
+
// 获取该群历史
|
|
149
|
+
const groupId = session.groupId;
|
|
150
|
+
const history = groupChats.get(groupId) || [];
|
|
151
|
+
|
|
152
|
+
// 如果没有历史,直接跳过注入
|
|
153
|
+
if (history.length === 0) return next();
|
|
154
|
+
|
|
155
|
+
// 构造前情提要
|
|
156
|
+
const contextPrefix = config.prefixTemplate(history);
|
|
157
|
+
|
|
158
|
+
// 获取原始内容并清洗 (移除 @ 机器人部分,避免重复)
|
|
159
|
+
let originalContent = session.content || '';
|
|
160
|
+
|
|
161
|
+
// 移除 @ 标记,让 AI 看到的更干净
|
|
162
|
+
if (config.cleanMentions) {
|
|
163
|
+
originalContent = originalContent.replace(new RegExp(`<@${session.selfId}>`, 'g'), '')
|
|
164
|
+
.replace(new RegExp(`@${session.selfId}`, 'g'), '')
|
|
165
|
+
.trim();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// --- 核心注入逻辑 ---
|
|
169
|
+
// 策略 A: 如果 session 已经有 messages 数组 (某些高级 AI 插件使用),尝试注入到 messages 头部
|
|
170
|
+
// 策略 B: 默认修改 session.content (兼容绝大多数简单 AI 插件,包括之前的 deepseek-chat)
|
|
171
|
+
|
|
172
|
+
if (Array.isArray(session.messages) && session.messages.length > 0) {
|
|
173
|
+
// 尝试兼容多消息模式:将前情提要作为第一条系统/用户消息插入
|
|
174
|
+
// 注意:这取决于 AI 插件如何处理 messages 数组。
|
|
175
|
+
// 最稳妥的方式还是修改 content,因为很多插件是 content -> messages 转换的。
|
|
176
|
+
// 这里我们优先保证 content 被修改,因为这是最通用的。
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// 执行注入:拼接内容
|
|
180
|
+
// 格式:[前情提要] + [用户当前问题]
|
|
181
|
+
session.content = `${contextPrefix}\n${originalContent}`;
|
|
182
|
+
|
|
183
|
+
if (config.verbose) {
|
|
184
|
+
console.log(`[GroupMemory] 群 ${groupId} 触发上下文注入。历史条数: ${history.length}`);
|
|
185
|
+
// console.log('Injected Content:', session.content);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return next();
|
|
189
|
+
}, 100);
|
|
190
|
+
};
|