mioku-plugin-chat 2.5.0 → 2.6.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/configs/groups.ts +5 -0
- package/context.ts +5 -3
- package/core/chat-turn.ts +2 -1
- package/core/media/image-analyzer.ts +87 -7
- package/core/tools/web.ts +1 -1
- package/db.ts +4 -1
- package/handlers/message.ts +7 -3
- package/handlers/poke.ts +2 -2
- package/humanize/emoji-agent.ts +20 -7
- package/humanize/emotion-agent.ts +33 -26
- package/humanize/expression.ts +24 -16
- package/humanize/index.ts +10 -8
- package/humanize/memory.ts +10 -7
- package/humanize/planner.ts +11 -16
- package/humanize/topic.ts +22 -15
- package/index.ts +115 -28
- package/manage/cooldown.ts +28 -21
- package/manage/idle-check.ts +14 -9
- package/manage/queue-processor.ts +22 -17
- package/manage/rate-limiter.ts +56 -80
- package/package.json +1 -1
- package/runtime/chat-runtime.ts +1 -1
- package/types.ts +23 -0
- package/utils/group-config.ts +70 -0
- package/utils/message.ts +78 -12
package/humanize/planner.ts
CHANGED
|
@@ -1,24 +1,25 @@
|
|
|
1
1
|
import type { AIInstance } from "mioku";
|
|
2
2
|
import { logger } from "mioki";
|
|
3
3
|
import type {
|
|
4
|
-
ChatConfig,
|
|
5
4
|
ChatMessage,
|
|
6
5
|
PlannerAction,
|
|
7
6
|
PlannerResult,
|
|
8
7
|
} from "../types";
|
|
8
|
+
import { extractGroupIdFromSession } from "../utils/group-config";
|
|
9
|
+
import type { ChatConfigProvider } from "./index";
|
|
9
10
|
|
|
10
11
|
export class ActionPlanner {
|
|
11
12
|
private ai: AIInstance;
|
|
12
|
-
private
|
|
13
|
+
private getConfig: ChatConfigProvider;
|
|
13
14
|
private actionHistory: Map<
|
|
14
15
|
string,
|
|
15
16
|
{ action: PlannerAction; time: number }[]
|
|
16
17
|
> = new Map();
|
|
17
18
|
private pendingPlans: Map<string, Promise<PlannerResult>> = new Map();
|
|
18
19
|
|
|
19
|
-
constructor(ai: AIInstance,
|
|
20
|
+
constructor(ai: AIInstance, configProvider: ChatConfigProvider) {
|
|
20
21
|
this.ai = ai;
|
|
21
|
-
this.
|
|
22
|
+
this.getConfig = configProvider;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
async plan(
|
|
@@ -28,18 +29,18 @@ export class ActionPlanner {
|
|
|
28
29
|
lastTriggerMessage: string,
|
|
29
30
|
isIdleCheck: boolean = false,
|
|
30
31
|
): Promise<PlannerResult> {
|
|
31
|
-
|
|
32
|
+
const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
|
|
33
|
+
if (!cfg.planner?.enabled) {
|
|
32
34
|
return { action: "reply", reason: "planner disabled" };
|
|
33
35
|
}
|
|
34
36
|
|
|
35
|
-
// Guard: if a plan call is already in-flight for this session, wait for it
|
|
36
37
|
const existing = this.pendingPlans.get(sessionId);
|
|
37
38
|
if (existing) {
|
|
38
39
|
logger.info(`[ActionPlanner] Session ${sessionId} has a plan already in-flight, waiting...`);
|
|
39
40
|
return existing;
|
|
40
41
|
}
|
|
41
42
|
|
|
42
|
-
const planPromise = this.doPlan(sessionId, botName, recentHistory, lastTriggerMessage, isIdleCheck);
|
|
43
|
+
const planPromise = this.doPlan(sessionId, botName, recentHistory, lastTriggerMessage, isIdleCheck, cfg);
|
|
43
44
|
this.pendingPlans.set(sessionId, planPromise);
|
|
44
45
|
|
|
45
46
|
try {
|
|
@@ -55,8 +56,9 @@ export class ActionPlanner {
|
|
|
55
56
|
recentHistory: ChatMessage[],
|
|
56
57
|
lastTriggerMessage: string,
|
|
57
58
|
isIdleCheck: boolean,
|
|
59
|
+
cfg = this.getConfig(extractGroupIdFromSession(sessionId)),
|
|
58
60
|
): Promise<PlannerResult> {
|
|
59
|
-
if (!
|
|
61
|
+
if (!cfg.planner?.enabled) {
|
|
60
62
|
return { action: "reply", reason: "planner disabled" };
|
|
61
63
|
}
|
|
62
64
|
|
|
@@ -81,7 +83,6 @@ export class ActionPlanner {
|
|
|
81
83
|
let prompt: string;
|
|
82
84
|
|
|
83
85
|
if (isIdleCheck) {
|
|
84
|
-
// 空闲检测模式:没有明确触发消息,观察群友聊天并决定是否要融入对话
|
|
85
86
|
prompt = `It is ${timeStr}. Your name is ${botName}.
|
|
86
87
|
|
|
87
88
|
Here is the recent chat content (from oldest to most recent):
|
|
@@ -120,7 +121,6 @@ OR for complete:
|
|
|
120
121
|
|
|
121
122
|
DO NOT include any explanation, markdown formatting, or additional text. Only output the JSON.`;
|
|
122
123
|
} else {
|
|
123
|
-
// 正常模式:由触发消息驱动的 planner
|
|
124
124
|
prompt = `It is ${timeStr}. Your name is ${botName}.
|
|
125
125
|
|
|
126
126
|
Here is the chat content:
|
|
@@ -169,12 +169,11 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
|
|
|
169
169
|
const content = await this.ai.generateText({
|
|
170
170
|
prompt,
|
|
171
171
|
messages: [],
|
|
172
|
-
model:
|
|
172
|
+
model: cfg.workingModel || cfg.model,
|
|
173
173
|
temperature: isIdleCheck ? 0.3 : 0.2,
|
|
174
174
|
max_tokens: isIdleCheck ? 300 : 500,
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
// 如果返回内容为空,使用默认值
|
|
178
177
|
if (!content || !content.trim()) {
|
|
179
178
|
logger.warn(
|
|
180
179
|
`[ActionPlanner] Empty response from AI, using default reply`,
|
|
@@ -182,7 +181,6 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
|
|
|
182
181
|
return { action: "reply", reason: "empty response" };
|
|
183
182
|
}
|
|
184
183
|
|
|
185
|
-
// 尝试提取 JSON 块
|
|
186
184
|
let jsonStr = "";
|
|
187
185
|
const jsonMatch = content.match(/\{[\s\S]*\}/);
|
|
188
186
|
if (jsonMatch) {
|
|
@@ -196,14 +194,11 @@ DO NOT include any explanation, markdown formatting, or additional text. Only ou
|
|
|
196
194
|
return { action: "reply", reason: "parse failed" };
|
|
197
195
|
}
|
|
198
196
|
|
|
199
|
-
// 尝试解析 JSON
|
|
200
197
|
let parsed: any;
|
|
201
198
|
try {
|
|
202
199
|
parsed = JSON.parse(jsonStr);
|
|
203
200
|
} catch {
|
|
204
|
-
// 尝试修复常见的 JSON 错误
|
|
205
201
|
try {
|
|
206
|
-
// 移除可能存在的尾随逗号
|
|
207
202
|
jsonStr = jsonStr.replace(/,\s*}/g, "}").replace(/,\s*]/g, "]");
|
|
208
203
|
parsed = JSON.parse(jsonStr);
|
|
209
204
|
} catch (e) {
|
package/humanize/topic.ts
CHANGED
|
@@ -2,25 +2,28 @@ import type { AIInstance } from "mioku";
|
|
|
2
2
|
import { logger } from "mioki";
|
|
3
3
|
import { extractJsonObject } from "../utils/json";
|
|
4
4
|
import type { ChatDatabase } from "../db";
|
|
5
|
-
import type {
|
|
5
|
+
import type { ChatMessage } from "../types";
|
|
6
|
+
import { extractGroupIdFromSession } from "../utils/group-config";
|
|
7
|
+
import type { ChatConfigProvider } from "./index";
|
|
6
8
|
|
|
7
9
|
export class TopicTracker {
|
|
8
10
|
private ai: AIInstance;
|
|
9
|
-
private
|
|
11
|
+
private getConfig: ChatConfigProvider;
|
|
10
12
|
private db: ChatDatabase;
|
|
11
13
|
private lastAnalyzedWindowEnd: Map<string, number> = new Map();
|
|
12
14
|
|
|
13
|
-
constructor(ai: AIInstance,
|
|
15
|
+
constructor(ai: AIInstance, configProvider: ChatConfigProvider, db: ChatDatabase) {
|
|
14
16
|
this.ai = ai;
|
|
15
|
-
this.
|
|
17
|
+
this.getConfig = configProvider;
|
|
16
18
|
this.db = db;
|
|
17
19
|
}
|
|
18
20
|
|
|
19
21
|
async onMessage(sessionId: string): Promise<void> {
|
|
20
|
-
|
|
22
|
+
const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
|
|
23
|
+
if (!cfg.topic?.enabled) return;
|
|
21
24
|
if (!sessionId.startsWith("group:")) return;
|
|
22
25
|
|
|
23
|
-
const windowMs = this.getWindowMs();
|
|
26
|
+
const windowMs = this.getWindowMs(cfg);
|
|
24
27
|
const completedWindowEnd = Math.floor(Date.now() / windowMs) * windowMs;
|
|
25
28
|
if (completedWindowEnd <= 0) return;
|
|
26
29
|
|
|
@@ -32,18 +35,19 @@ export class TopicTracker {
|
|
|
32
35
|
this.lastAnalyzedWindowEnd.set(sessionId, completedWindowEnd);
|
|
33
36
|
const windowStartAt = completedWindowEnd - windowMs;
|
|
34
37
|
|
|
35
|
-
this.analyzeWindow(sessionId, windowStartAt, completedWindowEnd).catch(
|
|
38
|
+
this.analyzeWindow(sessionId, windowStartAt, completedWindowEnd, cfg).catch(
|
|
36
39
|
(err) => logger.warn(`[TopicTracker] Analysis failed: ${err}`),
|
|
37
40
|
);
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
getTopicContext(sessionId: string, historyStartAt?: number): string {
|
|
41
|
-
|
|
44
|
+
const cfg = this.getConfig(extractGroupIdFromSession(sessionId));
|
|
45
|
+
if (!cfg.topic?.enabled) return "";
|
|
42
46
|
if (!sessionId.startsWith("group:")) return "";
|
|
43
47
|
if (!historyStartAt) return "";
|
|
44
48
|
|
|
45
|
-
const windowMs = this.getWindowMs();
|
|
46
|
-
const windowCount = this.getHistoryWindowCount();
|
|
49
|
+
const windowMs = this.getWindowMs(cfg);
|
|
50
|
+
const windowCount = this.getHistoryWindowCount(cfg);
|
|
47
51
|
const firstWindowEnd = Math.floor(historyStartAt / windowMs) * windowMs;
|
|
48
52
|
if (firstWindowEnd <= 0) return "";
|
|
49
53
|
|
|
@@ -79,6 +83,7 @@ export class TopicTracker {
|
|
|
79
83
|
sessionId: string,
|
|
80
84
|
windowStartAt: number,
|
|
81
85
|
windowEndAt: number,
|
|
86
|
+
cfg = this.getConfig(extractGroupIdFromSession(sessionId)),
|
|
82
87
|
): Promise<void> {
|
|
83
88
|
if (windowEndAt <= windowStartAt) return;
|
|
84
89
|
|
|
@@ -119,7 +124,7 @@ Output strictly in JSON format:
|
|
|
119
124
|
"summary": "one concise abstract summary sentence"
|
|
120
125
|
}`,
|
|
121
126
|
messages: [],
|
|
122
|
-
model:
|
|
127
|
+
model: cfg.workingModel || cfg.model,
|
|
123
128
|
temperature: 0.2,
|
|
124
129
|
max_tokens: 300,
|
|
125
130
|
});
|
|
@@ -163,14 +168,14 @@ Output strictly in JSON format:
|
|
|
163
168
|
}
|
|
164
169
|
}
|
|
165
170
|
|
|
166
|
-
private getWindowMs(): number {
|
|
167
|
-
const hours = Number(
|
|
171
|
+
private getWindowMs(cfg = this.getConfig()): number {
|
|
172
|
+
const hours = Number(cfg.topic?.windowHours);
|
|
168
173
|
const safeHours = Number.isFinite(hours) && hours > 0 ? hours : 5;
|
|
169
174
|
return Math.max(1, Math.floor(safeHours)) * 3600_000;
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
private getHistoryWindowCount(): number {
|
|
173
|
-
const count = Number(
|
|
177
|
+
private getHistoryWindowCount(cfg = this.getConfig()): number {
|
|
178
|
+
const count = Number(cfg.topic?.historyWindowCount);
|
|
174
179
|
if (!Number.isFinite(count) || count <= 0) return 3;
|
|
175
180
|
return Math.max(1, Math.floor(count));
|
|
176
181
|
}
|
|
@@ -202,3 +207,5 @@ Output strictly in JSON format:
|
|
|
202
207
|
return `${year}-${month}-${day} ${hour}:${minute}`;
|
|
203
208
|
}
|
|
204
209
|
}
|
|
210
|
+
|
|
211
|
+
export type { ChatMessage };
|
package/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { definePlugin } from "mioki";
|
|
|
2
2
|
import type { MiokiContext } from "mioki";
|
|
3
3
|
import type { AIInstance, AIService, ConfigService, ScreenshotService } from "mioku";
|
|
4
4
|
import { getPluginRuntimeState } from "mioku";
|
|
5
|
-
import type { ChatConfig, ChatMessage, TargetMessage } from "./types";
|
|
5
|
+
import type { ChatConfig, ChatMessage, ChatGroupsFile, TargetMessage } from "./types";
|
|
6
6
|
import { initDatabase } from "./db";
|
|
7
7
|
import { SessionManager } from "./manage/session";
|
|
8
8
|
import { RateLimiter } from "./manage/rate-limiter";
|
|
@@ -32,12 +32,15 @@ import {
|
|
|
32
32
|
import { BASE_CONFIG } from "./configs/base";
|
|
33
33
|
import { SETTINGS_CONFIG } from "./configs/settings";
|
|
34
34
|
import { PERSONALIZATION_CONFIG } from "./configs/personalization";
|
|
35
|
+
import { DEFAULT_GROUPS_CONFIG } from "./configs/groups";
|
|
35
36
|
import { RateLimitGuard } from "./manage/rate-limit-guard";
|
|
36
37
|
import { createChatRuntime } from "./runtime/chat-runtime";
|
|
37
38
|
import { createMessageHandler } from "./handlers/message";
|
|
38
39
|
import { createPokeHandler } from "./handlers/poke";
|
|
39
40
|
import { buildHistoryMediaOptions } from "./core/media/segment";
|
|
41
|
+
import type { ChatConfigProvider } from "./humanize";
|
|
40
42
|
import type { ChatPluginContext, ChatHandlerState } from "./context";
|
|
43
|
+
import { mergeGroupOverrides } from "./utils/group-config";
|
|
41
44
|
|
|
42
45
|
function normalizeIdList(input: unknown): number[] {
|
|
43
46
|
if (!Array.isArray(input)) return [];
|
|
@@ -50,6 +53,20 @@ function normalizeIdList(input: unknown): number[] {
|
|
|
50
53
|
);
|
|
51
54
|
}
|
|
52
55
|
|
|
56
|
+
function shallowMergeTopLevel<T extends Record<string, any>>(
|
|
57
|
+
defaults: T,
|
|
58
|
+
overrides: Record<string, any>,
|
|
59
|
+
): T {
|
|
60
|
+
const result: Record<string, any> = { ...defaults };
|
|
61
|
+
for (const key of Object.keys(overrides)) {
|
|
62
|
+
const value = overrides[key];
|
|
63
|
+
if (value !== undefined && value !== null) {
|
|
64
|
+
result[key] = value;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return result as T;
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
export default definePlugin({
|
|
54
71
|
name: "chat",
|
|
55
72
|
version: "1.0.0",
|
|
@@ -66,27 +83,36 @@ export default definePlugin({
|
|
|
66
83
|
await configService.registerConfig("chat", "base", BASE_CONFIG);
|
|
67
84
|
await configService.registerConfig("chat", "settings", SETTINGS_CONFIG);
|
|
68
85
|
await configService.registerConfig("chat", "personalization", PERSONALIZATION_CONFIG);
|
|
86
|
+
await configService.registerConfig("chat", "groups", DEFAULT_GROUPS_CONFIG);
|
|
69
87
|
}
|
|
70
88
|
|
|
71
|
-
|
|
89
|
+
let cachedBaseConfig: ChatConfig | null = null;
|
|
90
|
+
let cachedGroupsConfig: ChatGroupsFile | null = null;
|
|
91
|
+
|
|
92
|
+
const buildGlobalConfig = async (): Promise<ChatConfig> => {
|
|
72
93
|
if (!configService) {
|
|
73
|
-
return
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
94
|
+
return shallowMergeTopLevel(
|
|
95
|
+
shallowMergeTopLevel(
|
|
96
|
+
shallowMergeTopLevel({} as ChatConfig, BASE_CONFIG),
|
|
97
|
+
SETTINGS_CONFIG,
|
|
98
|
+
),
|
|
99
|
+
PERSONALIZATION_CONFIG,
|
|
100
|
+
);
|
|
78
101
|
}
|
|
79
|
-
const base = await configService.getConfig("chat", "base");
|
|
80
|
-
const settings = await configService.getConfig("chat", "settings");
|
|
81
|
-
const personalization =
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
102
|
+
const base = (await configService.getConfig("chat", "base")) ?? {};
|
|
103
|
+
const settings = (await configService.getConfig("chat", "settings")) ?? {};
|
|
104
|
+
const personalization =
|
|
105
|
+
(await configService.getConfig("chat", "personalization")) ?? {};
|
|
106
|
+
const merged = shallowMergeTopLevel(
|
|
107
|
+
shallowMergeTopLevel(
|
|
108
|
+
shallowMergeTopLevel({} as ChatConfig, BASE_CONFIG),
|
|
109
|
+
SETTINGS_CONFIG,
|
|
110
|
+
),
|
|
111
|
+
shallowMergeTopLevel(
|
|
112
|
+
shallowMergeTopLevel({} as ChatConfig, personalization),
|
|
113
|
+
base,
|
|
114
|
+
),
|
|
115
|
+
) as ChatConfig;
|
|
90
116
|
|
|
91
117
|
if (typeof merged.stream !== "boolean") merged.stream = true;
|
|
92
118
|
if (typeof merged.enableMarkdownScreenshot !== "boolean") {
|
|
@@ -102,13 +128,69 @@ export default definePlugin({
|
|
|
102
128
|
merged.whitelistGroups = normalizeIdList(merged.whitelistGroups);
|
|
103
129
|
merged.blacklistGroups = normalizeIdList(merged.blacklistGroups);
|
|
104
130
|
merged.mediaAnalysisBlacklistUsers = normalizeIdList(
|
|
105
|
-
merged.mediaAnalysisBlacklistUsers ?? merged.imageAnalysisBlacklistUsers,
|
|
131
|
+
(merged as any).mediaAnalysisBlacklistUsers ?? (merged as any).imageAnalysisBlacklistUsers,
|
|
106
132
|
);
|
|
107
|
-
delete merged.imageAnalysisBlacklistUsers;
|
|
108
|
-
return merged
|
|
133
|
+
delete (merged as any).imageAnalysisBlacklistUsers;
|
|
134
|
+
return merged;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
const refreshGroupsCache = async (): Promise<void> => {
|
|
138
|
+
if (!configService) {
|
|
139
|
+
cachedGroupsConfig = DEFAULT_GROUPS_CONFIG;
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
const raw = await configService.getConfig("chat", "groups");
|
|
143
|
+
cachedGroupsConfig =
|
|
144
|
+
raw && typeof raw === "object" && raw.groups && typeof raw.groups === "object"
|
|
145
|
+
? (raw as ChatGroupsFile)
|
|
146
|
+
: DEFAULT_GROUPS_CONFIG;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const refreshBaseCache = async (): Promise<void> => {
|
|
150
|
+
cachedBaseConfig = await buildGlobalConfig();
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
await refreshBaseCache();
|
|
154
|
+
await refreshGroupsCache();
|
|
155
|
+
|
|
156
|
+
if (configService) {
|
|
157
|
+
configService.onConfigChange("chat", "base", () => {
|
|
158
|
+
refreshBaseCache().catch((err) =>
|
|
159
|
+
ctx.logger.error(`刷新 chat/base 缓存失败: ${err}`),
|
|
160
|
+
);
|
|
161
|
+
});
|
|
162
|
+
configService.onConfigChange("chat", "settings", () => {
|
|
163
|
+
refreshBaseCache().catch((err) =>
|
|
164
|
+
ctx.logger.error(`刷新 chat/settings 缓存失败: ${err}`),
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
configService.onConfigChange("chat", "personalization", () => {
|
|
168
|
+
refreshBaseCache().catch((err) =>
|
|
169
|
+
ctx.logger.error(`刷新 chat/personalization 缓存失败: ${err}`),
|
|
170
|
+
);
|
|
171
|
+
});
|
|
172
|
+
configService.onConfigChange("chat", "groups", () => {
|
|
173
|
+
refreshGroupsCache().catch((err) =>
|
|
174
|
+
ctx.logger.error(`刷新 chat/groups 缓存失败: ${err}`),
|
|
175
|
+
);
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const configProvider: ChatConfigProvider = (groupId?: number) => {
|
|
180
|
+
const base = cachedBaseConfig;
|
|
181
|
+
if (!base) return PERSONALIZATION_CONFIG as unknown as ChatConfig;
|
|
182
|
+
if (groupId === undefined) return base;
|
|
183
|
+
const overrides = cachedGroupsConfig?.groups?.[String(groupId)];
|
|
184
|
+
return mergeGroupOverrides(base, overrides);
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const getConfig = async (groupId?: number): Promise<ChatConfig> => {
|
|
188
|
+
if (!cachedBaseConfig) await refreshBaseCache();
|
|
189
|
+
return configProvider(groupId);
|
|
109
190
|
};
|
|
110
191
|
|
|
111
|
-
const
|
|
192
|
+
const defaultConfig = configProvider();
|
|
193
|
+
const config = defaultConfig;
|
|
112
194
|
|
|
113
195
|
if (!config.apiKey) {
|
|
114
196
|
ctx.logger.warn("聊天插件未配置 API Key,请在 config/chat/base.json 中配置");
|
|
@@ -119,9 +201,12 @@ export default definePlugin({
|
|
|
119
201
|
const sessionManager = new SessionManager(db, config.maxSessions);
|
|
120
202
|
const queueManager = new MessageQueueManager();
|
|
121
203
|
const rateLimiter = new RateLimiter({
|
|
122
|
-
|
|
123
|
-
|
|
204
|
+
maxTriggersPerWindow: 5,
|
|
205
|
+
windowMs: 60_000,
|
|
206
|
+
dedupWindowMs: 30_000,
|
|
207
|
+
groupCooldownMs: 1_000,
|
|
124
208
|
});
|
|
209
|
+
rateLimiter.setConfigProvider(configProvider);
|
|
125
210
|
const skillManager = new SkillSessionManager();
|
|
126
211
|
|
|
127
212
|
rateLimiter.setQueueLengthGetter((groupId: number) =>
|
|
@@ -172,7 +257,7 @@ export default definePlugin({
|
|
|
172
257
|
});
|
|
173
258
|
}
|
|
174
259
|
|
|
175
|
-
const humanize = new HumanizeEngine(mainAIInstance, workAIInstance,
|
|
260
|
+
const humanize = new HumanizeEngine(mainAIInstance, workAIInstance, db, configProvider);
|
|
176
261
|
await humanize.init();
|
|
177
262
|
|
|
178
263
|
const pokeCooldowns = new Map<number, number>();
|
|
@@ -182,7 +267,8 @@ export default definePlugin({
|
|
|
182
267
|
const runWithRateLimitGuard = rateLimitGuard.run.bind(rateLimitGuard);
|
|
183
268
|
|
|
184
269
|
const pluginCtx = {
|
|
185
|
-
ctx,
|
|
270
|
+
ctx, defaultConfig, configProvider, getConfig,
|
|
271
|
+
db,
|
|
186
272
|
aiInstance: mainAIInstance, workAIInstance, visionAIInstance,
|
|
187
273
|
aiService, humanize,
|
|
188
274
|
sessionManager, skillManager, rateLimiter, queueManager,
|
|
@@ -209,12 +295,13 @@ export default definePlugin({
|
|
|
209
295
|
humanize.topicTracker.onMessage(groupSessionId),
|
|
210
296
|
]);
|
|
211
297
|
if (userMsg.groupId) {
|
|
298
|
+
const ttlMs = configProvider(userMsg.groupId).groupStructuredHistoryTtlMs;
|
|
212
299
|
groupStructuredHistory.append(
|
|
213
300
|
groupSessionId,
|
|
214
301
|
buildStructuredUserMessages([
|
|
215
302
|
buildStructuredUserInputFromTarget(userMsg as unknown as TargetMessage),
|
|
216
303
|
]),
|
|
217
|
-
|
|
304
|
+
ttlMs,
|
|
218
305
|
);
|
|
219
306
|
}
|
|
220
307
|
},
|
|
@@ -267,4 +354,4 @@ export default definePlugin({
|
|
|
267
354
|
ctx.logger.info("聊天插件已卸载");
|
|
268
355
|
};
|
|
269
356
|
},
|
|
270
|
-
});
|
|
357
|
+
});
|
package/manage/cooldown.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { MiokiContext } from "mioki";
|
|
|
2
2
|
import type { AIInstance } from "mioku";
|
|
3
3
|
import type { ChatConfig, TargetMessage } from "../types";
|
|
4
4
|
import type { ChatDatabase } from "../db";
|
|
5
|
-
import type { HumanizeEngine } from "../humanize";
|
|
5
|
+
import type { HumanizeEngine, ChatConfigProvider } from "../humanize";
|
|
6
6
|
import type { SkillSessionManager } from "./skill-session";
|
|
7
7
|
import type { GroupStructuredHistoryManager } from "./group-structured-history";
|
|
8
8
|
import { buildStructuredUserInputFromEvent } from "./group-structured-history";
|
|
@@ -41,7 +41,8 @@ export class CooldownManager {
|
|
|
41
41
|
private cooldownTimeoutIds = new Map<string, NodeJS.Timeout>();
|
|
42
42
|
|
|
43
43
|
private ctx: MiokiContext;
|
|
44
|
-
private
|
|
44
|
+
private configProvider: ChatConfigProvider;
|
|
45
|
+
private defaultConfig: ChatConfig;
|
|
45
46
|
private db: ChatDatabase;
|
|
46
47
|
private humanize: HumanizeEngine;
|
|
47
48
|
private aiInstance: AIInstance;
|
|
@@ -68,7 +69,8 @@ export class CooldownManager {
|
|
|
68
69
|
|
|
69
70
|
constructor(pluginCtx: ChatPluginContext) {
|
|
70
71
|
this.ctx = pluginCtx.ctx;
|
|
71
|
-
this.
|
|
72
|
+
this.configProvider = pluginCtx.configProvider;
|
|
73
|
+
this.defaultConfig = pluginCtx.defaultConfig;
|
|
72
74
|
this.db = pluginCtx.db;
|
|
73
75
|
this.humanize = pluginCtx.humanize;
|
|
74
76
|
this.aiInstance = pluginCtx.aiInstance;
|
|
@@ -99,7 +101,8 @@ export class CooldownManager {
|
|
|
99
101
|
const existingTimer = this.cooldownTimeoutIds.get(groupSessionId);
|
|
100
102
|
if (existingTimer) clearTimeout(existingTimer);
|
|
101
103
|
|
|
102
|
-
const
|
|
104
|
+
const cfg = this.configProvider(groupId);
|
|
105
|
+
const cooldownMs = cfg.cooldownAfterReplyMs ?? this.defaultConfig.cooldownAfterReplyMs ?? 20_000;
|
|
103
106
|
|
|
104
107
|
const timer = setTimeout(async () => {
|
|
105
108
|
this.cooldownTimeoutIds.delete(groupSessionId);
|
|
@@ -196,17 +199,19 @@ export class CooldownManager {
|
|
|
196
199
|
timestamp: Date.now(),
|
|
197
200
|
};
|
|
198
201
|
|
|
202
|
+
const cfg = this.configProvider(groupId);
|
|
203
|
+
|
|
199
204
|
const { history } = await this.getGroupHistoryMessages(
|
|
200
205
|
groupId,
|
|
201
206
|
groupSessionId,
|
|
202
207
|
this.ctx,
|
|
203
|
-
|
|
208
|
+
cfg.historyCount,
|
|
204
209
|
this.db,
|
|
205
210
|
selfId,
|
|
206
|
-
this.buildHistoryMediaOptions(this.aiInstance,
|
|
211
|
+
this.buildHistoryMediaOptions(this.aiInstance, cfg),
|
|
207
212
|
);
|
|
208
213
|
const botNickname =
|
|
209
|
-
|
|
214
|
+
cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
|
|
210
215
|
const botRole = await getBotRole(groupId, this.ctx, selfId);
|
|
211
216
|
const { groupName, memberCount } = await this.getGroupInfoData(
|
|
212
217
|
this.ctx,
|
|
@@ -220,7 +225,7 @@ export class CooldownManager {
|
|
|
220
225
|
groupSessionId,
|
|
221
226
|
groupId,
|
|
222
227
|
userId: targetMessage.userId,
|
|
223
|
-
config:
|
|
228
|
+
config: cfg,
|
|
224
229
|
aiService: this.aiService,
|
|
225
230
|
db: this.db,
|
|
226
231
|
botRole,
|
|
@@ -245,7 +250,7 @@ export class CooldownManager {
|
|
|
245
250
|
history,
|
|
246
251
|
targetMessage,
|
|
247
252
|
{
|
|
248
|
-
config:
|
|
253
|
+
config: cfg,
|
|
249
254
|
groupName,
|
|
250
255
|
memberCount,
|
|
251
256
|
botNickname,
|
|
@@ -266,7 +271,7 @@ export class CooldownManager {
|
|
|
266
271
|
this.skillManager,
|
|
267
272
|
{
|
|
268
273
|
manager: this.groupStructuredHistory,
|
|
269
|
-
ttlMs:
|
|
274
|
+
ttlMs: cfg.groupStructuredHistoryTtlMs,
|
|
270
275
|
currentUserInputs: collected.map((msg) =>
|
|
271
276
|
this.buildStructuredUserInputFromEvent(
|
|
272
277
|
msg.event,
|
|
@@ -285,7 +290,7 @@ export class CooldownManager {
|
|
|
285
290
|
ctx: this.ctx,
|
|
286
291
|
groupId,
|
|
287
292
|
messages: result.messages,
|
|
288
|
-
config:
|
|
293
|
+
config: cfg,
|
|
289
294
|
sentIndices: toolCtx.sentMessageIndices,
|
|
290
295
|
},
|
|
291
296
|
selfId,
|
|
@@ -298,7 +303,7 @@ export class CooldownManager {
|
|
|
298
303
|
groupSessionId,
|
|
299
304
|
result.messages,
|
|
300
305
|
now,
|
|
301
|
-
|
|
306
|
+
cfg,
|
|
302
307
|
this.db,
|
|
303
308
|
this.ctx,
|
|
304
309
|
selfId,
|
|
@@ -321,17 +326,19 @@ export class CooldownManager {
|
|
|
321
326
|
const mergedContent = collected.map((m) => m.content).join("\n");
|
|
322
327
|
const firstMsg = collected[0];
|
|
323
328
|
|
|
329
|
+
const cfg = this.configProvider(groupId);
|
|
330
|
+
|
|
324
331
|
const { history } = await this.getGroupHistoryMessages(
|
|
325
332
|
groupId,
|
|
326
333
|
groupSessionId,
|
|
327
334
|
this.ctx,
|
|
328
|
-
|
|
335
|
+
cfg.historyCount,
|
|
329
336
|
this.db,
|
|
330
337
|
selfId,
|
|
331
|
-
this.buildHistoryMediaOptions(this.aiInstance,
|
|
338
|
+
this.buildHistoryMediaOptions(this.aiInstance, cfg),
|
|
332
339
|
);
|
|
333
340
|
const botNickname =
|
|
334
|
-
|
|
341
|
+
cfg.nicknames[0] || this.ctx.pickBot(selfId).nickname || "Bot";
|
|
335
342
|
|
|
336
343
|
const planResult = await this.humanize.actionPlanner.plan(
|
|
337
344
|
groupSessionId,
|
|
@@ -362,7 +369,7 @@ export class CooldownManager {
|
|
|
362
369
|
groupSessionId,
|
|
363
370
|
groupId,
|
|
364
371
|
userId: targetMessage.userId,
|
|
365
|
-
config:
|
|
372
|
+
config: cfg,
|
|
366
373
|
aiService: this.aiService,
|
|
367
374
|
db: this.db,
|
|
368
375
|
botRole,
|
|
@@ -392,7 +399,7 @@ export class CooldownManager {
|
|
|
392
399
|
history,
|
|
393
400
|
targetMessage,
|
|
394
401
|
{
|
|
395
|
-
config:
|
|
402
|
+
config: cfg,
|
|
396
403
|
groupName,
|
|
397
404
|
memberCount,
|
|
398
405
|
botNickname,
|
|
@@ -418,7 +425,7 @@ export class CooldownManager {
|
|
|
418
425
|
this.skillManager,
|
|
419
426
|
{
|
|
420
427
|
manager: this.groupStructuredHistory,
|
|
421
|
-
ttlMs:
|
|
428
|
+
ttlMs: cfg.groupStructuredHistoryTtlMs,
|
|
422
429
|
currentUserInputs: collected.map((msg) =>
|
|
423
430
|
this.buildStructuredUserInputFromEvent(
|
|
424
431
|
msg.event,
|
|
@@ -442,7 +449,7 @@ export class CooldownManager {
|
|
|
442
449
|
ctx: this.ctx,
|
|
443
450
|
groupId,
|
|
444
451
|
messages: result.messages,
|
|
445
|
-
config:
|
|
452
|
+
config: cfg,
|
|
446
453
|
sentIndices: toolCtx.sentMessageIndices,
|
|
447
454
|
},
|
|
448
455
|
selfId,
|
|
@@ -454,7 +461,7 @@ export class CooldownManager {
|
|
|
454
461
|
groupSessionId,
|
|
455
462
|
result.messages,
|
|
456
463
|
now,
|
|
457
|
-
|
|
464
|
+
cfg,
|
|
458
465
|
this.db,
|
|
459
466
|
this.ctx,
|
|
460
467
|
selfId,
|
|
@@ -467,4 +474,4 @@ export class CooldownManager {
|
|
|
467
474
|
this.sessionManager.touch(groupSessionId);
|
|
468
475
|
this.startCooldownTimer(groupSessionId, groupId, selfId);
|
|
469
476
|
}
|
|
470
|
-
}
|
|
477
|
+
}
|
package/manage/idle-check.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { MiokiContext } from "mioki";
|
|
|
2
2
|
import type { AIInstance } from "mioku";
|
|
3
3
|
import type { ChatConfig, TargetMessage } from "../types";
|
|
4
4
|
import type { ChatDatabase } from "../db";
|
|
5
|
-
import type { HumanizeEngine } from "../humanize";
|
|
5
|
+
import type { HumanizeEngine, ChatConfigProvider } from "../humanize";
|
|
6
6
|
import type { SkillSessionManager } from "./skill-session";
|
|
7
7
|
import type { GroupStructuredHistoryManager } from "./group-structured-history";
|
|
8
8
|
import type { SessionManager } from "./session";
|
|
@@ -30,7 +30,8 @@ export class IdleCheckManager {
|
|
|
30
30
|
private intervalHandle: NodeJS.Timeout | null = null;
|
|
31
31
|
|
|
32
32
|
private ctx: MiokiContext;
|
|
33
|
-
private
|
|
33
|
+
private configProvider: ChatConfigProvider;
|
|
34
|
+
private defaultConfig: ChatConfig;
|
|
34
35
|
private db: ChatDatabase;
|
|
35
36
|
private humanize: HumanizeEngine;
|
|
36
37
|
private aiInstance: AIInstance;
|
|
@@ -50,7 +51,8 @@ export class IdleCheckManager {
|
|
|
50
51
|
|
|
51
52
|
constructor(pluginCtx: ChatPluginContext) {
|
|
52
53
|
this.ctx = pluginCtx.ctx;
|
|
53
|
-
this.
|
|
54
|
+
this.configProvider = pluginCtx.configProvider;
|
|
55
|
+
this.defaultConfig = pluginCtx.defaultConfig;
|
|
54
56
|
this.db = pluginCtx.db;
|
|
55
57
|
this.humanize = pluginCtx.humanize;
|
|
56
58
|
this.aiInstance = pluginCtx.aiInstance;
|
|
@@ -106,24 +108,27 @@ export class IdleCheckManager {
|
|
|
106
108
|
|
|
107
109
|
private async tick(): Promise<void> {
|
|
108
110
|
try {
|
|
109
|
-
const
|
|
110
|
-
if (!
|
|
111
|
+
const baseCfg = this.defaultConfig;
|
|
112
|
+
if (!baseCfg.apiKey) return;
|
|
111
113
|
|
|
112
114
|
const now = Date.now();
|
|
113
|
-
const idleThreshold = cfg.planner.idleThresholdMs ?? 30 * 60_000;
|
|
114
|
-
const messageCountThreshold = cfg.planner.idleMessageCount ?? 100;
|
|
115
115
|
const checkInterval = 60_000;
|
|
116
116
|
const allBotIds = Array.from(this.ctx.bots).map((bot) => bot.uin);
|
|
117
|
-
const idleCheckBotIds = cfg.planner.idleCheckBotIds ?? allBotIds;
|
|
118
|
-
const enabledBotIds = idleCheckBotIds.filter((id) => allBotIds.includes(id));
|
|
119
117
|
|
|
120
118
|
for (const [groupSessionId, lastTime] of this.groupLastActivityTime) {
|
|
121
119
|
const lastCheckTime = this.groupLastIdleCheckTime.get(groupSessionId) ?? 0;
|
|
122
120
|
if (now - lastCheckTime < checkInterval) continue;
|
|
123
121
|
|
|
124
122
|
const groupId = parseInt(groupSessionId.split(":")[1], 10);
|
|
123
|
+
const cfg = this.configProvider(groupId);
|
|
124
|
+
if (!cfg.planner?.enabled) continue;
|
|
125
125
|
if (!isGroupAllowed(groupId, cfg)) continue;
|
|
126
126
|
|
|
127
|
+
const idleThreshold = cfg.planner.idleThresholdMs ?? baseCfg.planner?.idleThresholdMs ?? 30 * 60_000;
|
|
128
|
+
const messageCountThreshold = cfg.planner.idleMessageCount ?? baseCfg.planner?.idleMessageCount ?? 100;
|
|
129
|
+
const idleCheckBotIds = cfg.planner.idleCheckBotIds ?? baseCfg.planner?.idleCheckBotIds ?? allBotIds;
|
|
130
|
+
const enabledBotIds = idleCheckBotIds.filter((id: number) => allBotIds.includes(id));
|
|
131
|
+
|
|
127
132
|
let lastBotTime = this.groupLastBotMessageTime.get(groupSessionId) ?? 0;
|
|
128
133
|
if (lastBotTime === 0) {
|
|
129
134
|
const botMsgs = this.db.getBotMessages(groupId, 1);
|