mioku-plugin-chat 2.7.0 → 2.8.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/base.ts +3 -9
- package/context.ts +2 -19
- package/core/tools/info.ts +17 -3
- package/handlers/message.ts +1 -1
- package/handlers/poke.ts +1 -1
- package/index.ts +175 -49
- package/manage/idle-check.ts +1 -1
- package/package.json +1 -1
- package/types.ts +5 -2
- package/utils/group-config.ts +9 -0
package/configs/base.ts
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
export const BASE_CONFIG = {
|
|
2
|
-
apiUrl: "https://api.openai.com/v1",
|
|
3
|
-
apiKey: "",
|
|
4
|
-
model: "gemini-3.0-flash-preview",
|
|
5
|
-
workingModel: "deepseek/deepseek-v3.2-exp", // 工作模型,用于 planner 等轻量任务
|
|
6
|
-
multimodalWorkingModel: "doubao-seed-2.0-mini", // 多模态工作模型,用于图片描述等任务
|
|
7
|
-
isMultimodal: true,
|
|
8
|
-
enableMediaRecognition: true, // 是否用多模态工作模型自动识别聊天记录中的图片/视频等媒体
|
|
9
2
|
maxContextTokens: 128,
|
|
10
3
|
temperature: 0.8,
|
|
11
|
-
historyCount: 100,
|
|
12
|
-
maxIterations: 20,
|
|
4
|
+
historyCount: 100,
|
|
5
|
+
maxIterations: 20,
|
|
6
|
+
enableMediaRecognition: true,
|
|
13
7
|
};
|
package/context.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { MiokiContext } from "mioki";
|
|
2
|
-
import type { AIInstance, AIService } from "mioku";
|
|
2
|
+
import type { AIInstance, AIModelRole, AIService } from "mioku";
|
|
3
3
|
import type { ChatConfig } from "./types";
|
|
4
4
|
import type { ChatDatabase } from "./db";
|
|
5
5
|
import type { HumanizeEngine, ChatConfigProvider } from "./humanize";
|
|
@@ -33,12 +33,7 @@ import type {
|
|
|
33
33
|
} from "mioku";
|
|
34
34
|
import type { ChatMessage } from "./types";
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
* Core plugin context - bundles all managers and core services.
|
|
38
|
-
* This is created once during plugin setup and used throughout the plugin lifetime.
|
|
39
|
-
*/
|
|
40
36
|
export interface ChatPluginContext {
|
|
41
|
-
// Core context
|
|
42
37
|
ctx: MiokiContext;
|
|
43
38
|
defaultConfig: ChatConfig;
|
|
44
39
|
configProvider: ChatConfigProvider;
|
|
@@ -47,10 +42,10 @@ export interface ChatPluginContext {
|
|
|
47
42
|
aiInstance: AIInstance;
|
|
48
43
|
workAIInstance: AIInstance;
|
|
49
44
|
visionAIInstance: AIInstance;
|
|
45
|
+
getAIInstance: (role?: AIModelRole) => AIInstance | undefined;
|
|
50
46
|
aiService: AIService;
|
|
51
47
|
humanize: HumanizeEngine;
|
|
52
48
|
|
|
53
|
-
// Managers
|
|
54
49
|
sessionManager: SessionManager;
|
|
55
50
|
skillManager: SkillSessionManager;
|
|
56
51
|
rateLimiter: RateLimiter;
|
|
@@ -61,10 +56,8 @@ export interface ChatPluginContext {
|
|
|
61
56
|
queueProcessor: QueueProcessor;
|
|
62
57
|
sessionTurnScheduler: SessionTurnScheduler;
|
|
63
58
|
|
|
64
|
-
// Utility
|
|
65
59
|
runWithRateLimitGuard: RunRateLimitGuard;
|
|
66
60
|
|
|
67
|
-
// Services (utility functions)
|
|
68
61
|
buildHistoryMediaOptions: (
|
|
69
62
|
ai: AIInstance,
|
|
70
63
|
config: ChatConfig,
|
|
@@ -90,17 +83,11 @@ export interface ChatPluginContext {
|
|
|
90
83
|
) => Promise<void>;
|
|
91
84
|
}
|
|
92
85
|
|
|
93
|
-
/**
|
|
94
|
-
* Runtime state for rate limiting - passed separately as it's mutable state.
|
|
95
|
-
*/
|
|
96
86
|
export interface ChatRuntimeState {
|
|
97
87
|
isRateLimitBlocked: () => boolean;
|
|
98
88
|
processingSet: Set<string>;
|
|
99
89
|
}
|
|
100
90
|
|
|
101
|
-
/**
|
|
102
|
-
* Closure state shared with the extracted message/poke handlers.
|
|
103
|
-
*/
|
|
104
91
|
export interface ChatHandlerState {
|
|
105
92
|
getConfig: (groupId?: number) => Promise<ChatConfig>;
|
|
106
93
|
matchMessageCommands?: (
|
|
@@ -110,10 +97,6 @@ export interface ChatHandlerState {
|
|
|
110
97
|
pokeCooldowns: Map<number, number>;
|
|
111
98
|
}
|
|
112
99
|
|
|
113
|
-
/**
|
|
114
|
-
* Chat runtime implementation for external callers (like boot plugin).
|
|
115
|
-
* Provides notice generation and information request capabilities.
|
|
116
|
-
*/
|
|
117
100
|
export interface ChatRuntime {
|
|
118
101
|
generateNotice(
|
|
119
102
|
options: ChatRuntimeNoticeOptions,
|
package/core/tools/info.ts
CHANGED
|
@@ -95,8 +95,17 @@ export function createInfoTools(toolCtx: ToolContext): AITool[] {
|
|
|
95
95
|
tools.push({
|
|
96
96
|
name: "get_group_member_list",
|
|
97
97
|
description: "Get the list of group members (returns name and role only)",
|
|
98
|
-
parameters: {
|
|
99
|
-
|
|
98
|
+
parameters: {
|
|
99
|
+
type: "object",
|
|
100
|
+
properties: {
|
|
101
|
+
limit: {
|
|
102
|
+
type: "number",
|
|
103
|
+
description: "Optional max members to return (server still caps at 50)",
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
required: [],
|
|
107
|
+
},
|
|
108
|
+
handler: async (args) => {
|
|
100
109
|
try {
|
|
101
110
|
const list = await toolCtx.ctx
|
|
102
111
|
.pickBot(toolCtx.event.self_id)
|
|
@@ -106,7 +115,12 @@ export function createInfoTools(toolCtx: ToolContext): AITool[] {
|
|
|
106
115
|
nickname: m.card || m.nickname,
|
|
107
116
|
role: m.role,
|
|
108
117
|
}));
|
|
109
|
-
|
|
118
|
+
const limitRaw = Math.floor(Number(args?.limit));
|
|
119
|
+
const limit =
|
|
120
|
+
Number.isFinite(limitRaw) && limitRaw > 0
|
|
121
|
+
? Math.min(limitRaw, 50)
|
|
122
|
+
: 50;
|
|
123
|
+
return { members: members.slice(0, limit), total: members.length };
|
|
110
124
|
} catch (err) {
|
|
111
125
|
return { error: `Failed to get member list: ${err}` };
|
|
112
126
|
}
|
package/handlers/message.ts
CHANGED
|
@@ -36,7 +36,7 @@ export function createMessageHandler(
|
|
|
36
36
|
const isGroup = e.message_type === "group";
|
|
37
37
|
const groupId: number | undefined = isGroup ? e.group_id : undefined;
|
|
38
38
|
const cfg = await getConfig(groupId);
|
|
39
|
-
if (!cfg.apiKey) return;
|
|
39
|
+
if (!cfg.model && !cfg.apiKey) return;
|
|
40
40
|
if (!e?.message || !Array.isArray(e.message)) return;
|
|
41
41
|
|
|
42
42
|
const text = ctx.text(e) || "";
|
package/handlers/poke.ts
CHANGED
|
@@ -16,7 +16,7 @@ export function createPokeHandler(
|
|
|
16
16
|
if (e.target_id !== e.self_id) return;
|
|
17
17
|
const groupId = e.group_id;
|
|
18
18
|
const cfg = groupId ? await getConfig(groupId) : await getConfig();
|
|
19
|
-
if (!cfg.apiKey) return;
|
|
19
|
+
if (!cfg.model && !cfg.apiKey) return;
|
|
20
20
|
if (!groupId || !isGroupAllowed(groupId, cfg)) return;
|
|
21
21
|
|
|
22
22
|
const lastPoke = pokeCooldowns.get(groupId) ?? 0;
|
package/index.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { definePlugin } from "mioki";
|
|
2
2
|
import type { MiokiContext } from "mioki";
|
|
3
|
-
import type { AIInstance, AIService, ConfigService, ScreenshotService } from "mioku";
|
|
3
|
+
import type { AIInstance, AIModelRole, AIService, ConfigService, ScreenshotService } from "mioku";
|
|
4
4
|
import { getPluginRuntimeState } from "mioku";
|
|
5
5
|
import type { ChatConfig, ChatMessage, ChatGroupsFile, TargetMessage } from "./types";
|
|
6
6
|
import { initDatabase } from "./db";
|
|
@@ -48,9 +48,69 @@ import {
|
|
|
48
48
|
normalizeMediaAnalysisBlacklist,
|
|
49
49
|
} from "./utils/config";
|
|
50
50
|
|
|
51
|
+
function resolveRoleInstances(aiService: AIService): {
|
|
52
|
+
main: AIInstance;
|
|
53
|
+
work: AIInstance;
|
|
54
|
+
vision: AIInstance;
|
|
55
|
+
models: { main: string; working: string; vision: string };
|
|
56
|
+
isMultimodal: boolean;
|
|
57
|
+
} | null {
|
|
58
|
+
const getByRole = (role: AIModelRole) =>
|
|
59
|
+
aiService.getInstanceByRole?.(role) ??
|
|
60
|
+
aiService.get?.(role) ??
|
|
61
|
+
undefined;
|
|
62
|
+
|
|
63
|
+
const main =
|
|
64
|
+
getByRole("main") ??
|
|
65
|
+
aiService.getDefault?.() ??
|
|
66
|
+
aiService.get?.("main");
|
|
67
|
+
if (!main) return null;
|
|
68
|
+
|
|
69
|
+
const work = getByRole("working") ?? aiService.get?.("work") ?? main;
|
|
70
|
+
const vision = getByRole("vision") ?? aiService.get?.("vision") ?? work;
|
|
71
|
+
|
|
72
|
+
const bindings = aiService.getRoleBindings?.() ?? {
|
|
73
|
+
main: undefined,
|
|
74
|
+
working: undefined,
|
|
75
|
+
vision: undefined,
|
|
76
|
+
};
|
|
77
|
+
const instances = aiService.listInstances?.() ?? [];
|
|
78
|
+
const findModel = (role: AIModelRole, fallbackInstance: AIInstance) => {
|
|
79
|
+
const full = bindings[role];
|
|
80
|
+
if (full && full.includes("/")) return full.split("/").slice(1).join("/");
|
|
81
|
+
const info = instances.find((item) => item.role === role || item.name === role);
|
|
82
|
+
if (info?.modelId) return info.modelId;
|
|
83
|
+
const anyInfo = instances.find((item) => item.name === (fallbackInstance as any).name);
|
|
84
|
+
return anyInfo?.modelId || "";
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const mainModel = findModel("main", main);
|
|
88
|
+
const workingModel = findModel("working", work) || mainModel;
|
|
89
|
+
const visionModel = findModel("vision", vision) || workingModel;
|
|
90
|
+
|
|
91
|
+
const models = aiService.listModels?.() ?? [];
|
|
92
|
+
const visionDesc =
|
|
93
|
+
models.find((item) => item.id === bindings.vision) ||
|
|
94
|
+
models.find((item) => item.modelId === visionModel);
|
|
95
|
+
const isMultimodal =
|
|
96
|
+
visionDesc?.capabilities?.includes("vision") ?? Boolean(visionModel);
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
main,
|
|
100
|
+
work,
|
|
101
|
+
vision,
|
|
102
|
+
models: {
|
|
103
|
+
main: mainModel,
|
|
104
|
+
working: workingModel,
|
|
105
|
+
vision: visionModel,
|
|
106
|
+
},
|
|
107
|
+
isMultimodal,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
51
111
|
export default definePlugin({
|
|
52
112
|
name: "chat",
|
|
53
|
-
version: "1.
|
|
113
|
+
version: "1.1.0",
|
|
54
114
|
description: "AI 智能聊天插件",
|
|
55
115
|
async setup(ctx: MiokiContext) {
|
|
56
116
|
ctx.logger.info("聊天插件正在初始化...");
|
|
@@ -69,6 +129,12 @@ export default definePlugin({
|
|
|
69
129
|
|
|
70
130
|
let cachedBaseConfig: ChatConfig | null = null;
|
|
71
131
|
let cachedGroupsConfig: ChatGroupsFile | null = null;
|
|
132
|
+
let roleModels = {
|
|
133
|
+
main: "",
|
|
134
|
+
working: "",
|
|
135
|
+
vision: "",
|
|
136
|
+
};
|
|
137
|
+
let roleIsMultimodal = true;
|
|
72
138
|
|
|
73
139
|
const buildGlobalConfig = async (): Promise<ChatConfig> => {
|
|
74
140
|
if (!configService) {
|
|
@@ -115,6 +181,15 @@ export default definePlugin({
|
|
|
115
181
|
merged.mediaAnalysisBlacklistUsers =
|
|
116
182
|
normalizeMediaAnalysisBlacklist(merged as Record<string, any>);
|
|
117
183
|
delete (merged as any).imageAnalysisBlacklistUsers;
|
|
184
|
+
|
|
185
|
+
merged.model = roleModels.main || merged.model || "";
|
|
186
|
+
merged.workingModel = roleModels.working || merged.workingModel || merged.model;
|
|
187
|
+
merged.multimodalWorkingModel =
|
|
188
|
+
roleModels.vision || merged.multimodalWorkingModel || merged.workingModel;
|
|
189
|
+
merged.isMultimodal = roleIsMultimodal;
|
|
190
|
+
if (!roleIsMultimodal) merged.enableMediaRecognition = false;
|
|
191
|
+
if (!merged.apiKey) merged.apiKey = "__ai_service__";
|
|
192
|
+
|
|
118
193
|
return merged;
|
|
119
194
|
};
|
|
120
195
|
|
|
@@ -173,63 +248,85 @@ export default definePlugin({
|
|
|
173
248
|
return configProvider(groupId);
|
|
174
249
|
};
|
|
175
250
|
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
if (!config.apiKey) {
|
|
180
|
-
ctx.logger.warn("聊天插件未配置 API Key,请在 config/chat/base.json 中配置");
|
|
251
|
+
if (!aiService) {
|
|
252
|
+
ctx.logger.error("聊天插件需要 AI 服务,但 AI 服务不可用");
|
|
181
253
|
return;
|
|
182
254
|
}
|
|
183
255
|
|
|
184
|
-
|
|
185
|
-
const sessionManager = new SessionManager(db, config.maxSessions);
|
|
186
|
-
const queueManager = new MessageQueueManager();
|
|
187
|
-
const rateLimiter = new RateLimiter({
|
|
188
|
-
maxTriggersPerWindow: 5,
|
|
189
|
-
windowMs: 60_000,
|
|
190
|
-
dedupWindowMs: 30_000,
|
|
191
|
-
groupCooldownMs: 1_000,
|
|
192
|
-
});
|
|
193
|
-
rateLimiter.setConfigProvider(configProvider);
|
|
194
|
-
const skillManager = new SkillSessionManager();
|
|
256
|
+
let resolved = resolveRoleInstances(aiService);
|
|
195
257
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
258
|
+
if (!resolved) {
|
|
259
|
+
const earlyConfig = configProvider();
|
|
260
|
+
const legacyApiUrl = String((earlyConfig as any).apiUrl || "").trim();
|
|
261
|
+
const legacyApiKey = String((earlyConfig as any).apiKey || "").trim();
|
|
262
|
+
const legacyModel = String((earlyConfig as any).model || "").trim();
|
|
263
|
+
if (legacyApiUrl && legacyApiKey && legacyApiKey !== "__ai_service__") {
|
|
264
|
+
ctx.logger.warn(
|
|
265
|
+
"未检测到 AI 角色绑定,回退到 chat/base 遗留 apiUrl/apiKey 创建实例",
|
|
266
|
+
);
|
|
267
|
+
await aiService.create({
|
|
268
|
+
name: "main",
|
|
269
|
+
apiUrl: legacyApiUrl,
|
|
270
|
+
apiKey: legacyApiKey,
|
|
271
|
+
modelType: "multimodal",
|
|
272
|
+
model: legacyModel || "default",
|
|
273
|
+
});
|
|
274
|
+
await aiService.create({
|
|
275
|
+
name: "work",
|
|
276
|
+
apiUrl: legacyApiUrl,
|
|
277
|
+
apiKey: legacyApiKey,
|
|
278
|
+
modelType: "text",
|
|
279
|
+
model: String((earlyConfig as any).workingModel || legacyModel || "default"),
|
|
280
|
+
});
|
|
281
|
+
await aiService.create({
|
|
282
|
+
name: "vision",
|
|
283
|
+
apiUrl: legacyApiUrl,
|
|
284
|
+
apiKey: legacyApiKey,
|
|
285
|
+
modelType: "multimodal",
|
|
286
|
+
model: String(
|
|
287
|
+
(earlyConfig as any).multimodalWorkingModel || legacyModel || "default",
|
|
288
|
+
),
|
|
289
|
+
});
|
|
290
|
+
aiService.setDefault("main");
|
|
291
|
+
aiService.setRoleBinding?.(
|
|
292
|
+
"main",
|
|
293
|
+
`runtime-main/${legacyModel || "default"}`,
|
|
294
|
+
);
|
|
295
|
+
aiService.setRoleBinding?.(
|
|
296
|
+
"working",
|
|
297
|
+
`runtime-work/${String((earlyConfig as any).workingModel || legacyModel || "default")}`,
|
|
298
|
+
);
|
|
299
|
+
aiService.setRoleBinding?.(
|
|
300
|
+
"vision",
|
|
301
|
+
`runtime-vision/${String((earlyConfig as any).multimodalWorkingModel || legacyModel || "default")}`,
|
|
302
|
+
);
|
|
303
|
+
resolved = resolveRoleInstances(aiService);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
199
306
|
|
|
200
|
-
if (!
|
|
201
|
-
ctx.logger.error(
|
|
307
|
+
if (!resolved) {
|
|
308
|
+
ctx.logger.error(
|
|
309
|
+
"未配置 AI 提供商/主模型,请在 WebUI AI 设置中配置提供商并绑定主模型",
|
|
310
|
+
);
|
|
202
311
|
return;
|
|
203
312
|
}
|
|
204
313
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
apiKey: config.apiKey,
|
|
216
|
-
modelType: "text",
|
|
217
|
-
model: config.workingModel,
|
|
218
|
-
});
|
|
219
|
-
const visionAIInstance = await aiService.create({
|
|
220
|
-
name: "vision",
|
|
221
|
-
apiUrl: config.apiUrl,
|
|
222
|
-
apiKey: config.apiKey,
|
|
223
|
-
modelType: "multimodal",
|
|
224
|
-
model: config.multimodalWorkingModel,
|
|
225
|
-
});
|
|
226
|
-
aiService.setDefault("main");
|
|
314
|
+
roleModels = resolved.models;
|
|
315
|
+
roleIsMultimodal = resolved.isMultimodal;
|
|
316
|
+
await refreshBaseCache();
|
|
317
|
+
|
|
318
|
+
const mainAIInstance = resolved.main;
|
|
319
|
+
const workAIInstance = resolved.work;
|
|
320
|
+
const visionAIInstance = resolved.vision;
|
|
321
|
+
if (!aiService.getDefault()) {
|
|
322
|
+
aiService.setDefault("main");
|
|
323
|
+
}
|
|
227
324
|
|
|
228
325
|
const registerPersona = (persona: string) => {
|
|
229
326
|
mainAIInstance.registerPrompt("persona", persona);
|
|
230
327
|
workAIInstance.registerPrompt("persona", persona);
|
|
231
328
|
};
|
|
232
|
-
registerPersona(String(
|
|
329
|
+
registerPersona(String(configProvider().persona ?? ""));
|
|
233
330
|
if (configService) {
|
|
234
331
|
configService.onConfigChange("chat", "personalization", async () => {
|
|
235
332
|
try {
|
|
@@ -241,6 +338,22 @@ export default definePlugin({
|
|
|
241
338
|
});
|
|
242
339
|
}
|
|
243
340
|
|
|
341
|
+
const db = await initDatabase();
|
|
342
|
+
const sessionManager = new SessionManager(db, configProvider().maxSessions);
|
|
343
|
+
const queueManager = new MessageQueueManager();
|
|
344
|
+
const rateLimiter = new RateLimiter({
|
|
345
|
+
maxTriggersPerWindow: 5,
|
|
346
|
+
windowMs: 60_000,
|
|
347
|
+
dedupWindowMs: 30_000,
|
|
348
|
+
groupCooldownMs: 1_000,
|
|
349
|
+
});
|
|
350
|
+
rateLimiter.setConfigProvider(configProvider);
|
|
351
|
+
const skillManager = new SkillSessionManager();
|
|
352
|
+
|
|
353
|
+
rateLimiter.setQueueLengthGetter((groupId: number) =>
|
|
354
|
+
queueManager.getQueueLength(`group:${groupId}`),
|
|
355
|
+
);
|
|
356
|
+
|
|
244
357
|
const humanize = new HumanizeEngine(workAIInstance, db, configProvider);
|
|
245
358
|
await humanize.init();
|
|
246
359
|
|
|
@@ -251,10 +364,21 @@ export default definePlugin({
|
|
|
251
364
|
const rateLimitGuard = new RateLimitGuard(rateLimiter, ctx.logger);
|
|
252
365
|
const runWithRateLimitGuard = rateLimitGuard.run.bind(rateLimitGuard);
|
|
253
366
|
|
|
367
|
+
const getAIInstance = (role: AIModelRole = "main"): AIInstance | undefined => {
|
|
368
|
+
if (role === "working") {
|
|
369
|
+
return aiService.getInstanceByRole?.("working") ?? workAIInstance;
|
|
370
|
+
}
|
|
371
|
+
if (role === "vision") {
|
|
372
|
+
return aiService.getInstanceByRole?.("vision") ?? visionAIInstance;
|
|
373
|
+
}
|
|
374
|
+
return aiService.getInstanceByRole?.("main") ?? mainAIInstance;
|
|
375
|
+
};
|
|
376
|
+
|
|
254
377
|
const pluginCtx = {
|
|
255
|
-
ctx, defaultConfig, configProvider, getConfig,
|
|
378
|
+
ctx, defaultConfig: configProvider(), configProvider, getConfig,
|
|
256
379
|
db,
|
|
257
380
|
aiInstance: mainAIInstance, workAIInstance, visionAIInstance,
|
|
381
|
+
getAIInstance,
|
|
258
382
|
aiService, humanize,
|
|
259
383
|
sessionManager, skillManager, rateLimiter, queueManager,
|
|
260
384
|
groupStructuredHistory,
|
|
@@ -301,7 +425,7 @@ export default definePlugin({
|
|
|
301
425
|
pluginCtx.queueProcessor = queueProcessor;
|
|
302
426
|
|
|
303
427
|
const cleanupInterval = setInterval(() => skillManager.cleanup(), 10 * 60_000);
|
|
304
|
-
const dbCleanup = new ChatDatabaseCleanup(db,
|
|
428
|
+
const dbCleanup = new ChatDatabaseCleanup(db, configProvider().retention ?? DEFAULT_CLEANUP_CONFIG);
|
|
305
429
|
dbCleanup.start();
|
|
306
430
|
idleCheckManager.start();
|
|
307
431
|
|
|
@@ -325,7 +449,9 @@ export default definePlugin({
|
|
|
325
449
|
ctx.handle("message", createMessageHandler(pluginCtx, handlerState));
|
|
326
450
|
ctx.handle("notice.group.poke" as any, createPokeHandler(pluginCtx, handlerState));
|
|
327
451
|
|
|
328
|
-
ctx.logger.info(
|
|
452
|
+
ctx.logger.info(
|
|
453
|
+
`聊天插件加载成功 (main=${roleModels.main || "?"}, work=${roleModels.working || "?"}, vision=${roleModels.vision || "?"})`,
|
|
454
|
+
);
|
|
329
455
|
|
|
330
456
|
return () => {
|
|
331
457
|
idleCheckManager.dispose();
|
package/manage/idle-check.ts
CHANGED
|
@@ -102,7 +102,7 @@ export class IdleCheckManager {
|
|
|
102
102
|
private async tick(): Promise<void> {
|
|
103
103
|
try {
|
|
104
104
|
const baseCfg = this.defaultConfig;
|
|
105
|
-
if (!baseCfg.apiKey) return;
|
|
105
|
+
if (!baseCfg.model && !baseCfg.apiKey) return;
|
|
106
106
|
|
|
107
107
|
const now = Date.now();
|
|
108
108
|
const checkInterval = 60_000;
|
package/package.json
CHANGED
package/types.ts
CHANGED
|
@@ -171,8 +171,10 @@ export interface AIRequestLimitConfig {
|
|
|
171
171
|
* 聊天插件配置
|
|
172
172
|
*/
|
|
173
173
|
export interface ChatConfig {
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
/** @deprecated AI 连接已迁移到 ai-service providers */
|
|
175
|
+
apiUrl?: string;
|
|
176
|
+
/** @deprecated AI 连接已迁移到 ai-service providers;非空仅作兼容哨兵 */
|
|
177
|
+
apiKey?: string;
|
|
176
178
|
model: string;
|
|
177
179
|
workingModel: string;
|
|
178
180
|
multimodalWorkingModel: string;
|
|
@@ -234,6 +236,7 @@ export interface ChatGroupOverrides {
|
|
|
234
236
|
dynamicDelay?: { enabled?: boolean };
|
|
235
237
|
enableMarkdownScreenshot?: boolean;
|
|
236
238
|
enableMediaRecognition?: boolean;
|
|
239
|
+
allowedExternalSkills?: string[];
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
export type ChatGroupsFile = {
|
package/utils/group-config.ts
CHANGED
|
@@ -18,6 +18,8 @@ const OVERRIDABLE_BOOLEAN_FIELDS = [
|
|
|
18
18
|
"enableMediaRecognition",
|
|
19
19
|
] as const;
|
|
20
20
|
|
|
21
|
+
const OVERRIDABLE_ARRAY_FIELDS = ["allowedExternalSkills"] as const;
|
|
22
|
+
|
|
21
23
|
/**
|
|
22
24
|
* 把每个群的覆盖配置浅合并到全局配置上
|
|
23
25
|
* - 子对象(如 emoji)只覆盖其中存在的字段,未填字段继承全局
|
|
@@ -55,6 +57,13 @@ export function mergeGroupOverrides(
|
|
|
55
57
|
}
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
for (const key of OVERRIDABLE_ARRAY_FIELDS) {
|
|
61
|
+
const value = overridesMap[key];
|
|
62
|
+
if (Array.isArray(value)) {
|
|
63
|
+
resultMap[key] = value.filter((v) => typeof v === "string");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
58
67
|
return result;
|
|
59
68
|
}
|
|
60
69
|
|