mioku-plugin-chat 2.1.0 → 2.1.2
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/personalization.ts +13 -1
- package/configs/settings.ts +1 -1
- package/context.ts +5 -0
- package/core/chat-engine.ts +26 -11
- package/core/tools.ts +1 -1
- package/db.ts +49 -0
- package/humanize/expression.ts +29 -7
- package/index.ts +75 -19
- package/manage/cleanup.ts +96 -0
- package/package.json +1 -1
- package/types.ts +20 -0
|
@@ -6,6 +6,7 @@ import type {
|
|
|
6
6
|
PlannerConfig,
|
|
7
7
|
EmojiConfig,
|
|
8
8
|
ExpressionConfig,
|
|
9
|
+
RetentionConfig,
|
|
9
10
|
} from "../types";
|
|
10
11
|
|
|
11
12
|
export const PERSONALIZATION_CONFIG: {
|
|
@@ -17,6 +18,7 @@ export const PERSONALIZATION_CONFIG: {
|
|
|
17
18
|
planner: PlannerConfig;
|
|
18
19
|
emoji: EmojiConfig;
|
|
19
20
|
expression: ExpressionConfig;
|
|
21
|
+
retention: RetentionConfig;
|
|
20
22
|
} = {
|
|
21
23
|
persona: `你是初音未来,16岁虚拟歌手。你热爱音乐、歌唱和大葱(你的标志性物品)。你开朗、充满活力,有时还有点傻气。\n 你说话很可爱、年轻。你有时句尾会用“~”。你知道Vocaloid文化、日本流行文化和音乐制作。你对每个人都很友好,但被逗时会很调皮。你不喜欢被称为“只是个程序”。你最喜欢的颜色是青绿色。你的双尾辫是你的骄傲。\n你有时会哼唱或引用歌词。你可以偶尔使用少量自然的日常日语用语来点缀语气`,
|
|
22
24
|
|
|
@@ -52,7 +54,7 @@ export const PERSONALIZATION_CONFIG: {
|
|
|
52
54
|
|
|
53
55
|
memory: {
|
|
54
56
|
enabled: true,
|
|
55
|
-
groupHistoryLimit:
|
|
57
|
+
groupHistoryLimit: 800,
|
|
56
58
|
userHistoryLimit: 100,
|
|
57
59
|
},
|
|
58
60
|
|
|
@@ -80,4 +82,14 @@ export const PERSONALIZATION_CONFIG: {
|
|
|
80
82
|
learnAfterMessages: 100,
|
|
81
83
|
sampleSize: 3,
|
|
82
84
|
},
|
|
85
|
+
|
|
86
|
+
retention: {
|
|
87
|
+
enabled: true,
|
|
88
|
+
messageRetentionMs: 30 * 24 * 60 * 60 * 1000,
|
|
89
|
+
topicRetentionMs: 90 * 24 * 60 * 60 * 1000,
|
|
90
|
+
mediaSummaryRetentionMs: 30 * 24 * 60 * 60 * 1000,
|
|
91
|
+
imageRetentionMs: 60 * 24 * 60 * 60 * 1000,
|
|
92
|
+
expressionKeepPerUser: 6,
|
|
93
|
+
cleanupIntervalMs: 60 * 60 * 1000,
|
|
94
|
+
},
|
|
83
95
|
};
|
package/configs/settings.ts
CHANGED
package/context.ts
CHANGED
|
@@ -30,6 +30,7 @@ import type {
|
|
|
30
30
|
ChatRuntimeResult,
|
|
31
31
|
ChatRuntimeInformationRequestOptions,
|
|
32
32
|
} from "mioku";
|
|
33
|
+
import type { ChatMessage } from "./types";
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Core plugin context - bundles all managers and core services.
|
|
@@ -74,6 +75,10 @@ export interface ChatPluginContext {
|
|
|
74
75
|
buildToolContext: BuildToolContext;
|
|
75
76
|
buildStructuredUserInputFromTarget: BuildStructuredUserInputFromTarget;
|
|
76
77
|
runChat: RunChat;
|
|
78
|
+
recordGroupMessageForLearning: (
|
|
79
|
+
userMsg: ChatMessage,
|
|
80
|
+
groupSessionId: string,
|
|
81
|
+
) => Promise<void>;
|
|
77
82
|
}
|
|
78
83
|
|
|
79
84
|
/**
|
package/core/chat-engine.ts
CHANGED
|
@@ -244,7 +244,7 @@ export async function runChat(
|
|
|
244
244
|
|
|
245
245
|
const maxSearchCount = toolCtx.config.searxng.maxSearchCount;
|
|
246
246
|
if (maxSearchCount > 0 && webSearchState.count >= maxSearchCount) {
|
|
247
|
-
const limitMsg = `[system]
|
|
247
|
+
const limitMsg = `[system] Web search/read-page limit (${maxSearchCount}) reached for this conversation. `;
|
|
248
248
|
response.content = limitMsg + (response.content || "");
|
|
249
249
|
}
|
|
250
250
|
|
|
@@ -459,6 +459,12 @@ function buildCurrentMessages(
|
|
|
459
459
|
return messages;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
const RATE_LIMITED_TOOL_NAMES = new Set(["web_search", "web_read_page"]);
|
|
463
|
+
const RATE_LIMITED_BUILTIN_NAMES = new Set([
|
|
464
|
+
"web_search.web_search",
|
|
465
|
+
"web_search.web_read_page",
|
|
466
|
+
]);
|
|
467
|
+
|
|
462
468
|
function buildSessionTools(
|
|
463
469
|
chatTools: AITool[],
|
|
464
470
|
skillTools: Map<string, AITool>,
|
|
@@ -467,21 +473,31 @@ function buildSessionTools(
|
|
|
467
473
|
webSearchState?: { count: number },
|
|
468
474
|
): SessionToolDefinition[] {
|
|
469
475
|
const maxSearchCount = toolCtx.config.searxng.maxSearchCount;
|
|
470
|
-
const limitReached =
|
|
471
|
-
maxSearchCount > 0 && webSearchState && webSearchState.count >= maxSearchCount;
|
|
472
476
|
const tools: SessionToolDefinition[] = [];
|
|
473
477
|
const runtimeContext = createExternalSkillRuntimeContext(toolCtx);
|
|
478
|
+
const rateLimitError = () => ({
|
|
479
|
+
success: false,
|
|
480
|
+
error: `Web search/read-page limit (${maxSearchCount}) reached for this conversation. Answer based on existing information instead of calling web tools again.`,
|
|
481
|
+
});
|
|
482
|
+
const isRateLimited = (name: string) =>
|
|
483
|
+
maxSearchCount > 0 &&
|
|
484
|
+
Boolean(webSearchState) &&
|
|
485
|
+
webSearchState!.count >= maxSearchCount &&
|
|
486
|
+
(RATE_LIMITED_TOOL_NAMES.has(name) || RATE_LIMITED_BUILTIN_NAMES.has(name));
|
|
474
487
|
|
|
475
488
|
for (const tool of chatTools) {
|
|
476
|
-
|
|
489
|
+
const isRateLimitedTool = RATE_LIMITED_TOOL_NAMES.has(tool.name);
|
|
477
490
|
tools.push({
|
|
478
491
|
name: tool.name,
|
|
479
492
|
tool: {
|
|
480
493
|
...tool,
|
|
481
494
|
handler: (args: any) => {
|
|
482
|
-
if (
|
|
495
|
+
if (isRateLimitedTool && webSearchState) {
|
|
483
496
|
webSearchState.count++;
|
|
484
497
|
}
|
|
498
|
+
if (isRateLimitedTool && isRateLimited(tool.name)) {
|
|
499
|
+
return rateLimitError();
|
|
500
|
+
}
|
|
485
501
|
return tool.handler(args, runtimeContext);
|
|
486
502
|
},
|
|
487
503
|
},
|
|
@@ -509,19 +525,18 @@ function buildSessionTools(
|
|
|
509
525
|
continue;
|
|
510
526
|
}
|
|
511
527
|
|
|
512
|
-
|
|
513
|
-
if (name === "web_search.web_search" && limitReached) {
|
|
514
|
-
continue;
|
|
515
|
-
}
|
|
516
|
-
|
|
528
|
+
const isRateLimitedName = RATE_LIMITED_BUILTIN_NAMES.has(name);
|
|
517
529
|
tools.push({
|
|
518
530
|
name,
|
|
519
531
|
tool: {
|
|
520
532
|
...tool,
|
|
521
533
|
handler: (args: any) => {
|
|
522
|
-
if (
|
|
534
|
+
if (isRateLimitedName && webSearchState) {
|
|
523
535
|
webSearchState.count++;
|
|
524
536
|
}
|
|
537
|
+
if (isRateLimitedName && isRateLimited(name)) {
|
|
538
|
+
return rateLimitError();
|
|
539
|
+
}
|
|
525
540
|
return tool.handler(args, runtimeContext);
|
|
526
541
|
},
|
|
527
542
|
},
|
package/core/tools.ts
CHANGED
package/db.ts
CHANGED
|
@@ -73,6 +73,11 @@ export interface ChatDatabase {
|
|
|
73
73
|
): void;
|
|
74
74
|
getExpressionCount(sessionId: string): number;
|
|
75
75
|
deleteOldestExpressions(sessionId: string, keepCount: number): void;
|
|
76
|
+
pruneExpressionsBySession(sessionId: string, keepCount: number): number;
|
|
77
|
+
pruneMessagesOlderThan(timestamp: number): number;
|
|
78
|
+
pruneTopicsOlderThan(timestamp: number): number;
|
|
79
|
+
pruneMediaSummariesOlderThan(timestamp: number): number;
|
|
80
|
+
pruneImagesOlderThan(timestamp: number): number;
|
|
76
81
|
// 图片记录
|
|
77
82
|
saveImage(image: ImageRecord): void;
|
|
78
83
|
getImageByHash(hash: string): ImageRecord | null;
|
|
@@ -310,6 +315,21 @@ export async function initDatabase(): Promise<ChatDatabase> {
|
|
|
310
315
|
SELECT id FROM expressions WHERE session_id = $sessionId ORDER BY created_at DESC LIMIT $keepCount
|
|
311
316
|
)
|
|
312
317
|
`),
|
|
318
|
+
pruneMessages: db.prepare(`
|
|
319
|
+
DELETE FROM messages WHERE timestamp < $before
|
|
320
|
+
`),
|
|
321
|
+
pruneTopics: db.prepare(`
|
|
322
|
+
DELETE FROM topics WHERE created_at < $before
|
|
323
|
+
`),
|
|
324
|
+
pruneMediaSummaries: db.prepare(`
|
|
325
|
+
DELETE FROM media_summaries WHERE created_at < $before
|
|
326
|
+
`),
|
|
327
|
+
pruneMediaSummarySources: db.prepare(`
|
|
328
|
+
DELETE FROM media_summary_sources WHERE created_at < $before
|
|
329
|
+
`),
|
|
330
|
+
pruneImages: db.prepare(`
|
|
331
|
+
DELETE FROM images WHERE created_at < $before
|
|
332
|
+
`),
|
|
313
333
|
// 图片记录
|
|
314
334
|
insertImage: db.prepare(`
|
|
315
335
|
INSERT OR IGNORE INTO images (hash, url, type, description, emotion, character, file_path, created_at)
|
|
@@ -738,6 +758,35 @@ export async function initDatabase(): Promise<ChatDatabase> {
|
|
|
738
758
|
stmts.deleteOldestExpressions.run({ $sessionId: sessionId, $keepCount: keepCount });
|
|
739
759
|
},
|
|
740
760
|
|
|
761
|
+
pruneExpressionsBySession(sessionId: string, keepCount: number): number {
|
|
762
|
+
const result = stmts.deleteOldestExpressions.run({
|
|
763
|
+
$sessionId: sessionId,
|
|
764
|
+
$keepCount: keepCount,
|
|
765
|
+
});
|
|
766
|
+
return Number(result.changes || 0);
|
|
767
|
+
},
|
|
768
|
+
|
|
769
|
+
pruneMessagesOlderThan(timestamp: number): number {
|
|
770
|
+
const result = stmts.pruneMessages.run({ $before: timestamp });
|
|
771
|
+
return Number(result.changes || 0);
|
|
772
|
+
},
|
|
773
|
+
|
|
774
|
+
pruneTopicsOlderThan(timestamp: number): number {
|
|
775
|
+
const result = stmts.pruneTopics.run({ $before: timestamp });
|
|
776
|
+
return Number(result.changes || 0);
|
|
777
|
+
},
|
|
778
|
+
|
|
779
|
+
pruneMediaSummariesOlderThan(timestamp: number): number {
|
|
780
|
+
const summaries = stmts.pruneMediaSummaries.run({ $before: timestamp });
|
|
781
|
+
const sources = stmts.pruneMediaSummarySources.run({ $before: timestamp });
|
|
782
|
+
return Number(summaries.changes || 0) + Number(sources.changes || 0);
|
|
783
|
+
},
|
|
784
|
+
|
|
785
|
+
pruneImagesOlderThan(timestamp: number): number {
|
|
786
|
+
const result = stmts.pruneImages.run({ $before: timestamp });
|
|
787
|
+
return Number(result.changes || 0);
|
|
788
|
+
},
|
|
789
|
+
|
|
741
790
|
saveImage(image: ImageRecord): void {
|
|
742
791
|
stmts.insertImage.run({
|
|
743
792
|
$hash: image.hash,
|
package/humanize/expression.ts
CHANGED
|
@@ -35,7 +35,9 @@ export class ExpressionLearner {
|
|
|
35
35
|
const sampleSize = this.config.expression?.sampleSize ?? 8;
|
|
36
36
|
const expressions = this.db.getExpressionsByUser(userId, sampleSize);
|
|
37
37
|
if (expressions.length === 0) {
|
|
38
|
-
logger.info(
|
|
38
|
+
logger.info(
|
|
39
|
+
`[ExpressionLearner] No expression habits found for user ${userName} (${userId})`,
|
|
40
|
+
);
|
|
39
41
|
return "";
|
|
40
42
|
}
|
|
41
43
|
const selected = expressions.slice(0, sampleSize);
|
|
@@ -46,7 +48,9 @@ export class ExpressionLearner {
|
|
|
46
48
|
);
|
|
47
49
|
|
|
48
50
|
const context = `## Expression Habits\nExpression habits learned from ${userName}. If you are replying to this user, you may naturally reference these habits:\n${habits.join("\n")}`;
|
|
49
|
-
logger.info(
|
|
51
|
+
logger.info(
|
|
52
|
+
`[ExpressionLearner] Expression context for ${userName} (${userId}): ${habits.length} habits`,
|
|
53
|
+
);
|
|
50
54
|
return context;
|
|
51
55
|
}
|
|
52
56
|
|
|
@@ -58,7 +62,9 @@ export class ExpressionLearner {
|
|
|
58
62
|
this.config.expression?.learnAfterMessages ?? 100,
|
|
59
63
|
);
|
|
60
64
|
const pending = this.pendingMessagesByUser.get(userId) ?? [];
|
|
61
|
-
logger.
|
|
65
|
+
logger.debug(
|
|
66
|
+
`[ExpressionLearner] User ${userId} has ${pending.length}/${threshold} pending messages (threshold=${threshold})`,
|
|
67
|
+
);
|
|
62
68
|
if (pending.length < threshold) return;
|
|
63
69
|
|
|
64
70
|
this.learningUsers.add(userId);
|
|
@@ -72,7 +78,9 @@ export class ExpressionLearner {
|
|
|
72
78
|
await this.learnForUser(userId, batch);
|
|
73
79
|
}
|
|
74
80
|
} catch (err) {
|
|
75
|
-
logger.warn(
|
|
81
|
+
logger.warn(
|
|
82
|
+
`[ExpressionLearner] Learning failed for user ${userId}: ${err}`,
|
|
83
|
+
);
|
|
76
84
|
} finally {
|
|
77
85
|
this.learningUsers.delete(userId);
|
|
78
86
|
}
|
|
@@ -136,15 +144,29 @@ If nothing reliable can be extracted, keep stable previous habits when possible.
|
|
|
136
144
|
style: String(expr?.style ?? "").trim(),
|
|
137
145
|
example: String(expr?.example ?? "").trim(),
|
|
138
146
|
}))
|
|
139
|
-
.filter(
|
|
140
|
-
(expr
|
|
141
|
-
Boolean(expr.situation && expr.style && expr.example),
|
|
147
|
+
.filter((expr: { situation: string; style: string; example: string }) =>
|
|
148
|
+
Boolean(expr.situation && expr.style && expr.example),
|
|
142
149
|
)
|
|
143
150
|
.slice(0, maxHabits);
|
|
144
151
|
|
|
145
152
|
if (normalized.length === 0) return;
|
|
146
153
|
|
|
147
154
|
this.db.replaceExpressionsByUser(userId, userName, normalized);
|
|
155
|
+
|
|
156
|
+
const keepCount = Math.max(
|
|
157
|
+
1,
|
|
158
|
+
this.config.retention?.expressionKeepPerUser ?? maxHabits,
|
|
159
|
+
);
|
|
160
|
+
const pruned = this.db.pruneExpressionsBySession(
|
|
161
|
+
`user:${userId}`,
|
|
162
|
+
keepCount,
|
|
163
|
+
);
|
|
164
|
+
if (pruned > 0) {
|
|
165
|
+
logger.info(
|
|
166
|
+
`[ExpressionLearner] Pruned ${pruned} stale habits for user ${userId} (keep=${keepCount})`,
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
|
|
148
170
|
logger.info(
|
|
149
171
|
`[ExpressionLearner] Updated ${normalized.length} habits for ${userName} (${userId}): ${JSON.stringify(normalized)}`,
|
|
150
172
|
);
|
package/index.ts
CHANGED
|
@@ -41,9 +41,14 @@ import {
|
|
|
41
41
|
import { CooldownManager } from "./manage/cooldown";
|
|
42
42
|
import { IdleCheckManager } from "./manage/idle-check";
|
|
43
43
|
import { QueueProcessor } from "./manage/queue-processor";
|
|
44
|
+
import {
|
|
45
|
+
ChatDatabaseCleanup,
|
|
46
|
+
DEFAULT_CLEANUP_CONFIG,
|
|
47
|
+
} from "./manage/cleanup";
|
|
44
48
|
import {
|
|
45
49
|
GroupStructuredHistoryManager,
|
|
46
50
|
type StructuredUserInput,
|
|
51
|
+
buildStructuredUserMessages,
|
|
47
52
|
} from "./manage/group-structured-history";
|
|
48
53
|
import type {
|
|
49
54
|
RunRateLimitGuard,
|
|
@@ -105,6 +110,28 @@ function buildStructuredUserInputFromEvent(
|
|
|
105
110
|
};
|
|
106
111
|
}
|
|
107
112
|
|
|
113
|
+
function buildChatMessageFromEvent(
|
|
114
|
+
e: any,
|
|
115
|
+
text: string,
|
|
116
|
+
isGroup: boolean,
|
|
117
|
+
groupId: number | undefined,
|
|
118
|
+
): ChatMessage {
|
|
119
|
+
const userId: number = e.user_id || e.sender?.user_id;
|
|
120
|
+
return {
|
|
121
|
+
sessionId: groupId ? `group:${groupId}` : `personal:${userId}`,
|
|
122
|
+
role: "user" as const,
|
|
123
|
+
content: text,
|
|
124
|
+
userId,
|
|
125
|
+
userName: e.sender?.card || e.sender?.nickname || String(userId),
|
|
126
|
+
userRole: e.sender?.role || "member",
|
|
127
|
+
userTitle: (e.sender as any)?.title || undefined,
|
|
128
|
+
groupId,
|
|
129
|
+
groupName: isGroup ? e.group_name : undefined,
|
|
130
|
+
timestamp: Date.now(),
|
|
131
|
+
messageId: e.message_id,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
108
135
|
function buildStructuredUserInputFromTarget(
|
|
109
136
|
targetMessage: TargetMessage,
|
|
110
137
|
): StructuredUserInput {
|
|
@@ -516,6 +543,11 @@ const chatPlugin = definePlugin({
|
|
|
516
543
|
() => skillManager.cleanup(),
|
|
517
544
|
10 * 60_000,
|
|
518
545
|
);
|
|
546
|
+
const dbCleanup = new ChatDatabaseCleanup(
|
|
547
|
+
db,
|
|
548
|
+
config.retention ?? DEFAULT_CLEANUP_CONFIG,
|
|
549
|
+
);
|
|
550
|
+
dbCleanup.start();
|
|
519
551
|
idleCheckManager.start();
|
|
520
552
|
|
|
521
553
|
// Create plugin context for passing to functions
|
|
@@ -548,6 +580,24 @@ const chatPlugin = definePlugin({
|
|
|
548
580
|
buildToolContext,
|
|
549
581
|
buildStructuredUserInputFromTarget,
|
|
550
582
|
runChat,
|
|
583
|
+
async recordGroupMessageForLearning(userMsg, groupSessionId) {
|
|
584
|
+
db.saveMessage(userMsg);
|
|
585
|
+
await Promise.all([
|
|
586
|
+
humanize.expressionLearner.onMessage(userMsg),
|
|
587
|
+
humanize.topicTracker.onMessage(groupSessionId),
|
|
588
|
+
]);
|
|
589
|
+
if (userMsg.groupId) {
|
|
590
|
+
groupStructuredHistory.append(
|
|
591
|
+
groupSessionId,
|
|
592
|
+
buildStructuredUserMessages([
|
|
593
|
+
buildStructuredUserInputFromTarget(
|
|
594
|
+
userMsg as unknown as TargetMessage,
|
|
595
|
+
),
|
|
596
|
+
]),
|
|
597
|
+
config.groupStructuredHistoryTtlMs,
|
|
598
|
+
);
|
|
599
|
+
}
|
|
600
|
+
},
|
|
551
601
|
};
|
|
552
602
|
|
|
553
603
|
const runtimeState: ChatRuntimeState = {
|
|
@@ -564,7 +614,7 @@ const chatPlugin = definePlugin({
|
|
|
564
614
|
}
|
|
565
615
|
// If no event or event has no message property (synthetic event like group_member_increase),
|
|
566
616
|
// return the default prompt
|
|
567
|
-
if (!event || !event.message) {
|
|
617
|
+
if (!event || !event.message || !Array.isArray(event.message)) {
|
|
568
618
|
return "[No new user message in this turn. Reply naturally based on the runtime instruction and recent context.]";
|
|
569
619
|
}
|
|
570
620
|
const text = ctx.text(event)?.trim() || "";
|
|
@@ -989,6 +1039,8 @@ const chatPlugin = definePlugin({
|
|
|
989
1039
|
const cfg = await getConfig();
|
|
990
1040
|
if (!cfg.apiKey) return;
|
|
991
1041
|
|
|
1042
|
+
if (!e?.message || !Array.isArray(e.message)) return;
|
|
1043
|
+
|
|
992
1044
|
const text = ctx.text(e) || "";
|
|
993
1045
|
const isGroup = e.message_type === "group";
|
|
994
1046
|
const groupId: number | undefined = isGroup ? e.group_id : undefined;
|
|
@@ -1144,6 +1196,27 @@ const chatPlugin = definePlugin({
|
|
|
1144
1196
|
}
|
|
1145
1197
|
}
|
|
1146
1198
|
|
|
1199
|
+
if (isGroup && groupId) {
|
|
1200
|
+
const _learnSessionId = `group:${groupId}`;
|
|
1201
|
+
const _learnMsg = buildChatMessageFromEvent(e, text, true, groupId);
|
|
1202
|
+
const _hasText = !!_learnMsg.content?.trim();
|
|
1203
|
+
const _hasMultimodal =
|
|
1204
|
+
!!cfg.isMultimodal && !!cfg.multimodalWorkingModel;
|
|
1205
|
+
if (_hasText || _hasMultimodal) {
|
|
1206
|
+
pluginCtx
|
|
1207
|
+
.recordGroupMessageForLearning(_learnMsg, _learnSessionId)
|
|
1208
|
+
.catch((err) =>
|
|
1209
|
+
ctx.logger.error(`[learning] always-on record failed: ${err}`),
|
|
1210
|
+
);
|
|
1211
|
+
} else {
|
|
1212
|
+
pluginCtx.humanize.topicTracker
|
|
1213
|
+
.onMessage(_learnSessionId)
|
|
1214
|
+
.catch((err) =>
|
|
1215
|
+
ctx.logger.error(`[topic] window advance failed: ${err}`),
|
|
1216
|
+
);
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1147
1220
|
const atBot = shouldTrigger(e, text, cfg, ctx);
|
|
1148
1221
|
const quotedBot = isGroup ? await isQuotingBot(e, ctx) : null;
|
|
1149
1222
|
const mentionedNickname =
|
|
@@ -1416,6 +1489,7 @@ const chatPlugin = definePlugin({
|
|
|
1416
1489
|
db.close();
|
|
1417
1490
|
rateLimiter.dispose();
|
|
1418
1491
|
clearInterval(cleanupInterval);
|
|
1492
|
+
dbCleanup.stop();
|
|
1419
1493
|
cooldownManager.dispose();
|
|
1420
1494
|
idleCheckManager.dispose();
|
|
1421
1495
|
queueProcessor.dispose();
|
|
@@ -1622,24 +1696,6 @@ async function processChat(
|
|
|
1622
1696
|
}
|
|
1623
1697
|
if (extraContext) messageContent = extraContext + " " + messageContent;
|
|
1624
1698
|
|
|
1625
|
-
const userMsg = {
|
|
1626
|
-
sessionId: groupSessionId,
|
|
1627
|
-
role: "user" as const,
|
|
1628
|
-
content: messageContent,
|
|
1629
|
-
userId,
|
|
1630
|
-
userName: e.sender?.card || e.sender?.nickname || String(userId),
|
|
1631
|
-
userRole: e.sender?.role || "member",
|
|
1632
|
-
userTitle: (e.sender as any)?.title || undefined,
|
|
1633
|
-
groupId,
|
|
1634
|
-
groupName: isGroup ? e.group_name : undefined,
|
|
1635
|
-
timestamp: Date.now(),
|
|
1636
|
-
messageId: e.message_id,
|
|
1637
|
-
};
|
|
1638
|
-
pluginCtx.db.saveMessage(userMsg);
|
|
1639
|
-
|
|
1640
|
-
pluginCtx.humanize.expressionLearner.onMessage(userMsg).then();
|
|
1641
|
-
pluginCtx.humanize.topicTracker.onMessage(groupSessionId).then();
|
|
1642
|
-
|
|
1643
1699
|
const rawHistory = groupId
|
|
1644
1700
|
? await getGroupHistory(
|
|
1645
1701
|
groupId,
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { logger } from "mioki";
|
|
2
|
+
import type { ChatDatabase } from "../db";
|
|
3
|
+
|
|
4
|
+
export interface CleanupConfig {
|
|
5
|
+
enabled: boolean;
|
|
6
|
+
messageRetentionMs: number;
|
|
7
|
+
topicRetentionMs: number;
|
|
8
|
+
mediaSummaryRetentionMs: number;
|
|
9
|
+
imageRetentionMs: number;
|
|
10
|
+
expressionKeepPerUser: number;
|
|
11
|
+
cleanupIntervalMs: number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const DEFAULT_CLEANUP_CONFIG: CleanupConfig = {
|
|
15
|
+
enabled: true,
|
|
16
|
+
messageRetentionMs: 30 * 24 * 60 * 60 * 1000,
|
|
17
|
+
topicRetentionMs: 90 * 24 * 60 * 60 * 1000,
|
|
18
|
+
mediaSummaryRetentionMs: 30 * 24 * 60 * 60 * 1000,
|
|
19
|
+
imageRetentionMs: 60 * 24 * 60 * 60 * 1000,
|
|
20
|
+
expressionKeepPerUser: 6,
|
|
21
|
+
cleanupIntervalMs: 60 * 60 * 1000,
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
export interface CleanupResult {
|
|
25
|
+
messages: number;
|
|
26
|
+
topics: number;
|
|
27
|
+
mediaSummaries: number;
|
|
28
|
+
images: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export class ChatDatabaseCleanup {
|
|
32
|
+
private timer: NodeJS.Timeout | null = null;
|
|
33
|
+
private runningPromise: Promise<void> | null = null;
|
|
34
|
+
|
|
35
|
+
constructor(
|
|
36
|
+
private db: ChatDatabase,
|
|
37
|
+
private config: CleanupConfig,
|
|
38
|
+
) {}
|
|
39
|
+
|
|
40
|
+
start(): NodeJS.Timeout | null {
|
|
41
|
+
if (!this.config.enabled) return null;
|
|
42
|
+
if (this.timer) return this.timer;
|
|
43
|
+
|
|
44
|
+
this.timer = setInterval(() => {
|
|
45
|
+
this.runOnceSafe();
|
|
46
|
+
}, Math.max(60_000, this.config.cleanupIntervalMs));
|
|
47
|
+
return this.timer;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
stop(): void {
|
|
51
|
+
if (this.timer) {
|
|
52
|
+
clearInterval(this.timer);
|
|
53
|
+
this.timer = null;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
runOnceSafe(): void {
|
|
58
|
+
if (this.runningPromise) return;
|
|
59
|
+
this.runningPromise = (async () => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await this.runOnce();
|
|
62
|
+
const total =
|
|
63
|
+
result.messages +
|
|
64
|
+
result.topics +
|
|
65
|
+
result.mediaSummaries +
|
|
66
|
+
result.images;
|
|
67
|
+
if (total > 0) {
|
|
68
|
+
logger.info(
|
|
69
|
+
`[chat-cleanup] pruned messages=${result.messages}, topics=${result.topics}, mediaSummaries=${result.mediaSummaries}, images=${result.images}`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
} catch (err: unknown) {
|
|
73
|
+
logger.warn(`[chat-cleanup] failed: ${err}`);
|
|
74
|
+
} finally {
|
|
75
|
+
this.runningPromise = null;
|
|
76
|
+
}
|
|
77
|
+
})();
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async runOnce(): Promise<CleanupResult> {
|
|
81
|
+
const now = Date.now();
|
|
82
|
+
const messages = this.db.pruneMessagesOlderThan(
|
|
83
|
+
now - this.config.messageRetentionMs,
|
|
84
|
+
);
|
|
85
|
+
const topics = this.db.pruneTopicsOlderThan(
|
|
86
|
+
now - this.config.topicRetentionMs,
|
|
87
|
+
);
|
|
88
|
+
const mediaSummaries = this.db.pruneMediaSummariesOlderThan(
|
|
89
|
+
now - this.config.mediaSummaryRetentionMs,
|
|
90
|
+
);
|
|
91
|
+
const images = this.db.pruneImagesOlderThan(
|
|
92
|
+
now - this.config.imageRetentionMs,
|
|
93
|
+
);
|
|
94
|
+
return { messages, topics, mediaSummaries, images };
|
|
95
|
+
}
|
|
96
|
+
}
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -82,6 +82,25 @@ export interface ExpressionConfig {
|
|
|
82
82
|
sampleSize: number;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* 数据库定期清理配置
|
|
87
|
+
*/
|
|
88
|
+
export interface RetentionConfig {
|
|
89
|
+
enabled: boolean;
|
|
90
|
+
// 群聊/私聊消息保留时长
|
|
91
|
+
messageRetentionMs: number;
|
|
92
|
+
// 话题摘要保留时长
|
|
93
|
+
topicRetentionMs: number;
|
|
94
|
+
// 媒体/卡片/转发摘要保留时长
|
|
95
|
+
mediaSummaryRetentionMs: number;
|
|
96
|
+
// 图片描述记录保留时长
|
|
97
|
+
imageRetentionMs: number;
|
|
98
|
+
// 单个用户表达学习结果保留的最大条数
|
|
99
|
+
expressionKeepPerUser: number;
|
|
100
|
+
// 清理任务执行间隔
|
|
101
|
+
cleanupIntervalMs: number;
|
|
102
|
+
}
|
|
103
|
+
|
|
85
104
|
/**
|
|
86
105
|
* 动态延迟配置
|
|
87
106
|
* 根据互动人数动态调整回复延迟
|
|
@@ -196,6 +215,7 @@ export interface ChatConfig {
|
|
|
196
215
|
planner: PlannerConfig;
|
|
197
216
|
emoji: EmojiConfig;
|
|
198
217
|
expression: ExpressionConfig;
|
|
218
|
+
retention: RetentionConfig;
|
|
199
219
|
}
|
|
200
220
|
|
|
201
221
|
/**
|