mioku-plugin-chat 2.3.1 → 2.4.1
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/configs/base.ts +1 -0
- package/configs/settings.ts +1 -1
- package/context.ts +17 -0
- package/core/chat-engine.ts +6 -1
- package/core/chat-turn.ts +540 -0
- package/core/media/history-media.ts +215 -70
- package/core/media/image-analyzer.ts +28 -19
- package/core/media/image-compress.ts +75 -0
- package/core/media/segment.ts +91 -0
- package/core/multimodal.ts +24 -12
- package/core/prompt.ts +332 -445
- package/core/tools/info.ts +284 -0
- package/core/tools/load-skill.ts +149 -0
- package/core/tools/web.ts +306 -0
- package/core/tools.ts +11 -822
- package/db.ts +56 -142
- package/handlers/idle-debug.ts +135 -0
- package/handlers/message.ts +316 -0
- package/handlers/poke.ts +165 -0
- package/humanize/emotion-agent.ts +3 -4
- package/humanize/expression.ts +3 -4
- package/humanize/topic.ts +3 -4
- package/index.ts +73 -1705
- package/manage/cooldown.ts +52 -27
- package/manage/group-structured-history.ts +35 -0
- package/manage/idle-check.ts +40 -20
- package/manage/queue-processor.ts +51 -25
- package/manage/rate-limit-guard.ts +70 -0
- package/package.json +1 -1
- package/runtime/chat-runtime.ts +124 -0
- package/types.ts +1 -0
- package/utils/index.ts +2 -0
- package/utils/json.ts +8 -0
- package/utils/message.ts +185 -175
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { logger } from "mioki";
|
|
2
|
+
import type { AITool } from "mioku";
|
|
3
|
+
import type { ChatMessage, ToolContext } from "../../types";
|
|
4
|
+
import { searchWebWithSearxng } from "../web/searxng";
|
|
5
|
+
import { readWebPage } from "../web/web-reader";
|
|
6
|
+
import { MemoryRetrieval } from "../../humanize";
|
|
7
|
+
import type { MemoryUserHistoryChunk } from "../../humanize/memory";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_GROUP_RECALL_LIMIT = 800;
|
|
10
|
+
const DEFAULT_USER_HISTORY_LIMIT = 100;
|
|
11
|
+
|
|
12
|
+
function resolveGroupRecallLimit(value: unknown): number {
|
|
13
|
+
const parsed = Number(value);
|
|
14
|
+
if (!Number.isFinite(parsed)) return DEFAULT_GROUP_RECALL_LIMIT;
|
|
15
|
+
return Math.max(1, Math.floor(parsed));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function resolveUserHistoryLimit(value: unknown): number {
|
|
19
|
+
const parsed = Number(value);
|
|
20
|
+
if (!Number.isFinite(parsed)) return DEFAULT_USER_HISTORY_LIMIT;
|
|
21
|
+
return Math.max(1, Math.floor(parsed));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function extractTargetUserIdsFromQuestion(
|
|
25
|
+
question: string,
|
|
26
|
+
requesterUserId: number,
|
|
27
|
+
): number[] {
|
|
28
|
+
const matches = question.match(/\b\d{5,12}\b/g) || [];
|
|
29
|
+
const parsed = matches
|
|
30
|
+
.map((item) => Number(item))
|
|
31
|
+
.filter((item) => Number.isFinite(item) && item > 0);
|
|
32
|
+
const ids = [requesterUserId, ...parsed].filter(
|
|
33
|
+
(item) => Number.isFinite(item) && item > 0,
|
|
34
|
+
);
|
|
35
|
+
return [...new Set(ids)].slice(0, 3);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function extractGroupHistoryText(raw: any): string {
|
|
39
|
+
const segments = Array.isArray(raw?.message) ? raw.message : [];
|
|
40
|
+
if (segments.length === 0) return String(raw?.raw_message || "").trim();
|
|
41
|
+
|
|
42
|
+
const parts: string[] = [];
|
|
43
|
+
for (const seg of segments) {
|
|
44
|
+
if (seg?.type === "text") {
|
|
45
|
+
const text = String(seg?.data?.text || "");
|
|
46
|
+
if (text) parts.push(text);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (seg?.type === "at") {
|
|
50
|
+
const target = seg?.qq || seg?.data?.qq || seg?.data?.id || seg?.data?.user_id;
|
|
51
|
+
if (target === "all" || target === "everyone") parts.push("@全体成员");
|
|
52
|
+
else if (target) parts.push(`@${target}`);
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (seg?.type === "image") {
|
|
56
|
+
parts.push("[image]");
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const joined = parts.join(" ").trim();
|
|
62
|
+
return joined || String(raw?.raw_message || "").trim();
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function fetchGroupHistoryByMessageIdPaging(
|
|
66
|
+
toolCtx: ToolContext,
|
|
67
|
+
limit: number,
|
|
68
|
+
): Promise<ChatMessage[]> {
|
|
69
|
+
if (!toolCtx.groupId || limit <= 0) return [];
|
|
70
|
+
|
|
71
|
+
const selfId = Number(toolCtx.event?.self_id || 0);
|
|
72
|
+
if (!selfId) return [];
|
|
73
|
+
|
|
74
|
+
const bot = toolCtx.ctx.pickBot(selfId);
|
|
75
|
+
if (!bot) return [];
|
|
76
|
+
|
|
77
|
+
const collected: ChatMessage[] = [];
|
|
78
|
+
const seenMessageIds = new Set<string>();
|
|
79
|
+
let cursorMessageId = 0;
|
|
80
|
+
const maxPages = Math.max(1, Math.ceil(limit / 200) + 5);
|
|
81
|
+
let page = 0;
|
|
82
|
+
|
|
83
|
+
while (collected.length < limit && page < maxPages) {
|
|
84
|
+
const remaining = limit - collected.length;
|
|
85
|
+
const pageSize = Math.min(200, remaining);
|
|
86
|
+
|
|
87
|
+
let response: any;
|
|
88
|
+
try {
|
|
89
|
+
response = await (bot as any).api("get_group_msg_history", {
|
|
90
|
+
group_id: String(toolCtx.groupId),
|
|
91
|
+
message_seq: String(cursorMessageId),
|
|
92
|
+
count: pageSize,
|
|
93
|
+
reverse_order: false,
|
|
94
|
+
disable_get_url: true,
|
|
95
|
+
parse_mult_msg: false,
|
|
96
|
+
quick_reply: false,
|
|
97
|
+
});
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const errText = String(err);
|
|
100
|
+
if (
|
|
101
|
+
cursorMessageId > 0 &&
|
|
102
|
+
(errText.includes("不存在") || errText.toLowerCase().includes("not exist"))
|
|
103
|
+
) {
|
|
104
|
+
logger.info(`[recall_memory] get_group_msg_history stop at cursor ${cursorMessageId}: ${errText}`);
|
|
105
|
+
} else {
|
|
106
|
+
logger.warn(`[recall_memory] get_group_msg_history failed at cursor ${cursorMessageId}: ${errText}`);
|
|
107
|
+
}
|
|
108
|
+
break;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const rawMessages = response?.messages || response?.data?.messages || [];
|
|
112
|
+
if (!Array.isArray(rawMessages) || rawMessages.length === 0) break;
|
|
113
|
+
|
|
114
|
+
let oldestMessageId: number | null = null;
|
|
115
|
+
let newAdded = 0;
|
|
116
|
+
|
|
117
|
+
for (const raw of rawMessages) {
|
|
118
|
+
const messageId = Number(raw?.message_id || raw?.message_seq || 0);
|
|
119
|
+
const key =
|
|
120
|
+
messageId > 0
|
|
121
|
+
? `mid:${messageId}`
|
|
122
|
+
: `${String(raw?.user_id || "unknown")}:${String(raw?.time || "0")}:${String(raw?.raw_message || "")}`;
|
|
123
|
+
if (seenMessageIds.has(key)) continue;
|
|
124
|
+
seenMessageIds.add(key);
|
|
125
|
+
|
|
126
|
+
const content = extractGroupHistoryText(raw);
|
|
127
|
+
if (!content.trim()) continue;
|
|
128
|
+
|
|
129
|
+
const ts = typeof raw?.time === "number" ? raw.time * 1000 : Date.now();
|
|
130
|
+
collected.push({
|
|
131
|
+
sessionId: toolCtx.sessionId,
|
|
132
|
+
role: String(raw?.user_id) === String(selfId) ? "assistant" : "user",
|
|
133
|
+
content,
|
|
134
|
+
userId: typeof raw?.user_id === "number" ? raw.user_id : Number(raw?.user_id),
|
|
135
|
+
userName: raw?.sender?.card || raw?.sender?.nickname || String(raw?.user_id || "unknown"),
|
|
136
|
+
userRole: raw?.sender?.role || "member",
|
|
137
|
+
groupId: toolCtx.groupId,
|
|
138
|
+
timestamp: ts,
|
|
139
|
+
messageId: messageId > 0 ? messageId : undefined,
|
|
140
|
+
});
|
|
141
|
+
newAdded += 1;
|
|
142
|
+
|
|
143
|
+
if (messageId > 0 && (oldestMessageId === null || messageId < oldestMessageId)) {
|
|
144
|
+
oldestMessageId = messageId;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (newAdded === 0 || oldestMessageId === null || oldestMessageId <= 1) break;
|
|
149
|
+
|
|
150
|
+
// NapCat message_seq expects an existing id; don't subtract 1 (IDs may be non-contiguous).
|
|
151
|
+
const nextCursor = oldestMessageId;
|
|
152
|
+
if (nextCursor === cursorMessageId) break;
|
|
153
|
+
cursorMessageId = nextCursor;
|
|
154
|
+
page += 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
collected.sort((a, b) => {
|
|
158
|
+
if (a.timestamp !== b.timestamp) return a.timestamp - b.timestamp;
|
|
159
|
+
return (a.messageId || 0) - (b.messageId || 0);
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
return collected.length > limit ? collected.slice(-limit) : collected;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
export function createWebSearchTool(toolCtx: ToolContext): AITool {
|
|
166
|
+
return {
|
|
167
|
+
name: "web_search",
|
|
168
|
+
description:
|
|
169
|
+
"Search the web using SearXNG. Use this for current events, external facts, documentation, or anything not in chat history. This tool can only be called a limited number of times per conversation.",
|
|
170
|
+
parameters: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
query: { type: "string", description: "Search query" },
|
|
174
|
+
queries: {
|
|
175
|
+
type: "array",
|
|
176
|
+
items: { type: "string" },
|
|
177
|
+
description:
|
|
178
|
+
"Alternative input. Multiple search queries; only the first non-empty query will be used.",
|
|
179
|
+
},
|
|
180
|
+
limit: {
|
|
181
|
+
type: "number",
|
|
182
|
+
description: "Max number of results to return. Will be clamped by config maxLimit.",
|
|
183
|
+
},
|
|
184
|
+
time_range: {
|
|
185
|
+
type: "string",
|
|
186
|
+
enum: ["day", "month", "year"],
|
|
187
|
+
description: "Optional time filter for recent results",
|
|
188
|
+
},
|
|
189
|
+
categories: {
|
|
190
|
+
type: "array",
|
|
191
|
+
items: { type: "string" },
|
|
192
|
+
description: 'Optional categories, e.g. ["general"], ["news"], ["science"]',
|
|
193
|
+
},
|
|
194
|
+
engines: {
|
|
195
|
+
type: "array",
|
|
196
|
+
items: { type: "string" },
|
|
197
|
+
description: 'Optional engines, e.g. ["google"], ["bing"], ["duckduckgo"]',
|
|
198
|
+
},
|
|
199
|
+
},
|
|
200
|
+
required: [],
|
|
201
|
+
},
|
|
202
|
+
handler: async (args) => searchWebWithSearxng(toolCtx.config.searxng, args || {}),
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export function createWebReadPageTool(toolCtx: ToolContext): AITool {
|
|
207
|
+
return {
|
|
208
|
+
name: "web_read_page",
|
|
209
|
+
description:
|
|
210
|
+
"Read a webpage by URL, extract its main content, and compress the content into a short, information-dense passage. Use this directly when the user already provides a URL, or combine with web_search when you need to discover relevant pages first.",
|
|
211
|
+
parameters: {
|
|
212
|
+
type: "object",
|
|
213
|
+
properties: {
|
|
214
|
+
url: { type: "string", description: "The http/https URL of the webpage to read" },
|
|
215
|
+
render_js: {
|
|
216
|
+
type: "boolean",
|
|
217
|
+
description:
|
|
218
|
+
"Set true only if the page likely requires JavaScript rendering. This uses much more CPU and memory.",
|
|
219
|
+
},
|
|
220
|
+
question: {
|
|
221
|
+
type: "string",
|
|
222
|
+
description:
|
|
223
|
+
"Optional question or focus. The tool will prioritize webpage details relevant to this question.",
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
required: ["url"],
|
|
227
|
+
},
|
|
228
|
+
handler: async (args) => {
|
|
229
|
+
try {
|
|
230
|
+
const ai = toolCtx.config.webReader.useWorkingModel
|
|
231
|
+
? toolCtx.aiService.getDefault()
|
|
232
|
+
: undefined;
|
|
233
|
+
if (toolCtx.config.webReader.useWorkingModel && !ai) {
|
|
234
|
+
return { success: false, error: "AI instance not available" };
|
|
235
|
+
}
|
|
236
|
+
return await readWebPage(
|
|
237
|
+
ai,
|
|
238
|
+
toolCtx.config.workingModel || toolCtx.config.model,
|
|
239
|
+
toolCtx.config.webReader,
|
|
240
|
+
args || {},
|
|
241
|
+
);
|
|
242
|
+
} catch (err) {
|
|
243
|
+
return { success: false, error: `Failed to read webpage: ${err}` };
|
|
244
|
+
}
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export function createRecallMemoryTool(toolCtx: ToolContext): AITool {
|
|
250
|
+
return {
|
|
251
|
+
name: "recall_memory",
|
|
252
|
+
description:
|
|
253
|
+
"Ask the memory worker model to retrieve historical chat context for a recall question. Use only when recall is explicitly needed and the answer is not already in current context.",
|
|
254
|
+
parameters: {
|
|
255
|
+
type: "object",
|
|
256
|
+
properties: {
|
|
257
|
+
question: {
|
|
258
|
+
type: "string",
|
|
259
|
+
description:
|
|
260
|
+
"The recall question to investigate, e.g. 'What did user 123 mention about travel plans before?'",
|
|
261
|
+
},
|
|
262
|
+
},
|
|
263
|
+
required: ["question"],
|
|
264
|
+
},
|
|
265
|
+
handler: async (args) => {
|
|
266
|
+
const question = String(args?.question || "").trim();
|
|
267
|
+
if (!question) return { success: false, error: "question is required" };
|
|
268
|
+
|
|
269
|
+
const ai = toolCtx.aiService.getDefault();
|
|
270
|
+
if (!ai) return { success: false, error: "AI instance not available" };
|
|
271
|
+
|
|
272
|
+
const groupHistoryLimit = resolveGroupRecallLimit(toolCtx.config.memory?.groupHistoryLimit);
|
|
273
|
+
const userHistoryLimit = resolveUserHistoryLimit(toolCtx.config.memory?.userHistoryLimit);
|
|
274
|
+
const groupHistoryMessages = await fetchGroupHistoryByMessageIdPaging(toolCtx, groupHistoryLimit);
|
|
275
|
+
const targetUserIds = extractTargetUserIdsFromQuestion(question, toolCtx.userId);
|
|
276
|
+
const userHistories: MemoryUserHistoryChunk[] = targetUserIds.map((userId) => ({
|
|
277
|
+
userId,
|
|
278
|
+
messages: toolCtx.db.getMessagesByUser(userId, toolCtx.sessionId, userHistoryLimit),
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
const retriever = new MemoryRetrieval(ai, toolCtx.config, toolCtx.db);
|
|
282
|
+
const answer = await retriever.retrieveByQuestion({
|
|
283
|
+
sessionId: toolCtx.sessionId,
|
|
284
|
+
question,
|
|
285
|
+
nowTimestamp: Date.now(),
|
|
286
|
+
groupHistoryMessages,
|
|
287
|
+
userHistories,
|
|
288
|
+
});
|
|
289
|
+
const queriedAt = new Date().toLocaleString("zh-CN", { hour12: false });
|
|
290
|
+
return {
|
|
291
|
+
success: true,
|
|
292
|
+
queried_at: queriedAt,
|
|
293
|
+
question,
|
|
294
|
+
found: Boolean(answer),
|
|
295
|
+
answer: answer || "",
|
|
296
|
+
group_history_count: groupHistoryMessages.length,
|
|
297
|
+
group_history_limit: groupHistoryLimit,
|
|
298
|
+
user_history_limit: userHistoryLimit,
|
|
299
|
+
user_history_targets: targetUserIds,
|
|
300
|
+
note: answer
|
|
301
|
+
? "Memory worker retrieved historical context."
|
|
302
|
+
: "Memory worker did not find useful historical context.",
|
|
303
|
+
};
|
|
304
|
+
},
|
|
305
|
+
};
|
|
306
|
+
}
|