mioku-plugin-chat 2.8.3 → 2.9.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/settings.ts +0 -3
- package/context.ts +2 -0
- package/core/base.ts +17 -3
- package/core/chat-turn.ts +2 -2
- package/core/media/audio.ts +52 -61
- package/core/prompt.ts +2 -2
- package/handlers/message.ts +7 -0
- package/handlers/tts-command.ts +67 -0
- package/index.ts +100 -38
- package/manage/types.ts +2 -0
- package/package.json +8 -2
- package/types.ts +0 -5
package/configs/settings.ts
CHANGED
package/context.ts
CHANGED
|
@@ -31,6 +31,7 @@ import type {
|
|
|
31
31
|
ChatRuntimeResult,
|
|
32
32
|
ChatRuntimeInformationRequestOptions,
|
|
33
33
|
} from "mioku";
|
|
34
|
+
import type { AudioServiceApi } from "mioku-service-audio";
|
|
34
35
|
import type { ChatMessage } from "./types";
|
|
35
36
|
|
|
36
37
|
export interface ChatPluginContext {
|
|
@@ -81,6 +82,7 @@ export interface ChatPluginContext {
|
|
|
81
82
|
userMsg: ChatMessage,
|
|
82
83
|
groupSessionId: string,
|
|
83
84
|
) => Promise<void>;
|
|
85
|
+
audioService?: AudioServiceApi;
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
export interface ChatRuntimeState {
|
package/core/base.ts
CHANGED
|
@@ -14,7 +14,8 @@ import type { HumanizeEngine } from "../humanize";
|
|
|
14
14
|
import { parseLineMarkers, splitByReplyMarkers } from "../utils/queue";
|
|
15
15
|
import { getGroupHistory } from "../utils";
|
|
16
16
|
import { getService, Services } from "mioku";
|
|
17
|
-
import {
|
|
17
|
+
import { synthesizeAudioSource } from "./media/audio";
|
|
18
|
+
import type { AudioServiceApi } from "mioku-service-audio";
|
|
18
19
|
import {
|
|
19
20
|
extractStandaloneMarkdownBlock,
|
|
20
21
|
splitOutgoingUnits,
|
|
@@ -29,6 +30,7 @@ export interface SendAIResponseOptions {
|
|
|
29
30
|
config: ChatConfig;
|
|
30
31
|
sentIndices?: Set<number>;
|
|
31
32
|
onLineSent?: () => void | Promise<void>;
|
|
33
|
+
audioService?: AudioServiceApi;
|
|
32
34
|
}
|
|
33
35
|
|
|
34
36
|
const FAST_TYPING_BASE_MS = 150;
|
|
@@ -85,6 +87,7 @@ export async function sendAIResponse(
|
|
|
85
87
|
config,
|
|
86
88
|
sentIndices,
|
|
87
89
|
onLineSent,
|
|
90
|
+
audioService,
|
|
88
91
|
} = options;
|
|
89
92
|
const typingDelayEnabled = config.enableTypingDelay ?? false;
|
|
90
93
|
const enableMarkdownScreenshot = config.enableMarkdownScreenshot ?? true;
|
|
@@ -205,6 +208,7 @@ export async function sendAIResponse(
|
|
|
205
208
|
const audioSource = await resolveAudioSource(ctx, {
|
|
206
209
|
audioText,
|
|
207
210
|
config,
|
|
211
|
+
audioService,
|
|
208
212
|
});
|
|
209
213
|
const fallbackText = !audioSource && audioText ? audioText : undefined;
|
|
210
214
|
if (sendableText) {
|
|
@@ -243,6 +247,7 @@ export async function sendMessage(
|
|
|
243
247
|
text: string,
|
|
244
248
|
config: ChatConfig,
|
|
245
249
|
selfId: number,
|
|
250
|
+
audioService?: AudioServiceApi,
|
|
246
251
|
): Promise<void> {
|
|
247
252
|
const typingDelayEnabled = config.enableTypingDelay ?? false;
|
|
248
253
|
const enableMarkdownScreenshot = config.enableMarkdownScreenshot ?? true;
|
|
@@ -353,6 +358,7 @@ export async function sendMessage(
|
|
|
353
358
|
const audioSource = await resolveAudioSource(ctx, {
|
|
354
359
|
audioText,
|
|
355
360
|
config,
|
|
361
|
+
audioService,
|
|
356
362
|
});
|
|
357
363
|
const fallbackText = !audioSource && audioText ? audioText : undefined;
|
|
358
364
|
|
|
@@ -610,6 +616,7 @@ async function resolveAudioSource(
|
|
|
610
616
|
options: {
|
|
611
617
|
audioText?: string;
|
|
612
618
|
config: ChatConfig;
|
|
619
|
+
audioService?: AudioServiceApi;
|
|
613
620
|
},
|
|
614
621
|
): Promise<string | null> {
|
|
615
622
|
const trimmed = String(options.audioText || "").trim();
|
|
@@ -617,12 +624,16 @@ async function resolveAudioSource(
|
|
|
617
624
|
return null;
|
|
618
625
|
}
|
|
619
626
|
|
|
620
|
-
if (!options.config.audio?.enabled
|
|
627
|
+
if (!options.config.audio?.enabled) {
|
|
628
|
+
return null;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
if (!options.audioService) {
|
|
621
632
|
return null;
|
|
622
633
|
}
|
|
623
634
|
|
|
624
635
|
try {
|
|
625
|
-
return await
|
|
636
|
+
return await synthesizeAudioSource(options.audioService, trimmed);
|
|
626
637
|
} catch (error) {
|
|
627
638
|
ctx.logger.error(
|
|
628
639
|
`[audio] Failed to synthesize voice for "${trimmed}": ${error}`,
|
|
@@ -746,6 +757,7 @@ export interface BuildToolContextOptions {
|
|
|
746
757
|
pendingImageUrls?: string[];
|
|
747
758
|
humanize: HumanizeEngine;
|
|
748
759
|
targetMessage: TargetMessage;
|
|
760
|
+
audioService?: AudioServiceApi;
|
|
749
761
|
}
|
|
750
762
|
|
|
751
763
|
function resolveTriggerSkillRole(
|
|
@@ -794,6 +806,7 @@ export function buildToolContext(
|
|
|
794
806
|
pendingImageUrls,
|
|
795
807
|
humanize,
|
|
796
808
|
targetMessage,
|
|
809
|
+
audioService,
|
|
797
810
|
} = options;
|
|
798
811
|
|
|
799
812
|
return {
|
|
@@ -818,6 +831,7 @@ export function buildToolContext(
|
|
|
818
831
|
content,
|
|
819
832
|
config,
|
|
820
833
|
selfId,
|
|
834
|
+
audioService,
|
|
821
835
|
);
|
|
822
836
|
},
|
|
823
837
|
};
|
package/core/chat-turn.ts
CHANGED
|
@@ -148,7 +148,7 @@ export async function finalizeChatTurn(
|
|
|
148
148
|
|
|
149
149
|
if (groupId) {
|
|
150
150
|
await pluginCtx.sendAIResponse(
|
|
151
|
-
{ ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices },
|
|
151
|
+
{ ctx, groupId, messages: result.messages, config: cfg, sentIndices: toolCtx.sentMessageIndices, audioService: pluginCtx.audioService },
|
|
152
152
|
selfId,
|
|
153
153
|
);
|
|
154
154
|
await pluginCtx.sendEmoji(ctx, groupId, result.emojiPath, selfId);
|
|
@@ -162,7 +162,7 @@ export async function finalizeChatTurn(
|
|
|
162
162
|
const sentIndices = toolCtx.sentMessageIndices;
|
|
163
163
|
for (let i = 0; i < result.messages.length; i++) {
|
|
164
164
|
if (sentIndices?.has(i)) continue;
|
|
165
|
-
await pluginCtx.sendMessage(ctx, undefined, userId, result.messages[i], cfg, selfId);
|
|
165
|
+
await pluginCtx.sendMessage(ctx, undefined, userId, result.messages[i], cfg, selfId, pluginCtx.audioService);
|
|
166
166
|
}
|
|
167
167
|
if (result.emojiPath) {
|
|
168
168
|
try {
|
package/core/media/audio.ts
CHANGED
|
@@ -1,73 +1,64 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { readFile } from "node:fs/promises";
|
|
2
|
+
import type { MiokiContext } from "mioki";
|
|
3
|
+
import type { AudioServiceApi, SupportedLang } from "mioku-service-audio";
|
|
2
4
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
function ensureTrailingSlash(value: string): string {
|
|
10
|
-
return value.endsWith("/") ? value : `${value}/`;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function detectAudioLanguage(text: string): string {
|
|
14
|
-
if (/[\u3040-\u30ff]/.test(text)) {
|
|
15
|
-
return "all_ja";
|
|
16
|
-
}
|
|
17
|
-
if (/[\u4e00-\u9fff]/.test(text)) {
|
|
18
|
-
return "all_zh";
|
|
19
|
-
}
|
|
5
|
+
function detectAudioLanguage(text: string): SupportedLang {
|
|
6
|
+
if (/[\u3040-\u30ff]/.test(text)) return "ja";
|
|
7
|
+
if (/[\u4e00-\u9fff]/.test(text)) return "zh";
|
|
8
|
+
if (/[\uac00-\ud7af]/.test(text)) return "ko";
|
|
20
9
|
return "en";
|
|
21
10
|
}
|
|
22
11
|
|
|
23
|
-
export async function
|
|
24
|
-
|
|
12
|
+
export async function synthesizeAudioSource(
|
|
13
|
+
audioService: AudioServiceApi | undefined,
|
|
25
14
|
text: string,
|
|
26
|
-
): Promise<string> {
|
|
27
|
-
const
|
|
28
|
-
if (!
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
if (!trimmedText) {
|
|
32
|
-
throw new Error("Audio text cannot be empty");
|
|
33
|
-
}
|
|
34
|
-
if (!config.baseUrl?.trim()) {
|
|
35
|
-
throw new Error("Audio service baseUrl is not configured");
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
const endpoint = new URL("tts", ensureTrailingSlash(config.baseUrl.trim()));
|
|
39
|
-
const headers: Record<string, string> = {
|
|
40
|
-
"Content-Type": "application/json",
|
|
41
|
-
};
|
|
15
|
+
): Promise<string | null> {
|
|
16
|
+
const trimmed = String(text || "").trim();
|
|
17
|
+
if (!audioService) return null;
|
|
18
|
+
if (!trimmed) return null;
|
|
42
19
|
|
|
43
|
-
const
|
|
44
|
-
if (
|
|
45
|
-
headers["X-API-Key"] = apiKey;
|
|
46
|
-
}
|
|
20
|
+
const isReady = await audioService.ready().catch(() => false);
|
|
21
|
+
if (!isReady) return null;
|
|
47
22
|
|
|
48
|
-
const
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
text: trimmedText,
|
|
53
|
-
text_lang: detectAudioLanguage(trimmedText),
|
|
54
|
-
}),
|
|
55
|
-
signal: AbortSignal.timeout(Math.max(1000, config.timeoutMs || 20_000)),
|
|
23
|
+
const result = await audioService.generateByText({
|
|
24
|
+
text: trimmed,
|
|
25
|
+
textLang: detectAudioLanguage(trimmed),
|
|
26
|
+
mediaType: "wav",
|
|
56
27
|
});
|
|
57
28
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
`TTS request failed with status ${response.status}`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
29
|
+
if (!result?.filePath) return null;
|
|
30
|
+
const filePath = result.filePath.replace(/^file:\/\//, "");
|
|
31
|
+
const buf = await readFile(filePath);
|
|
32
|
+
return `base64://${buf.toString("base64")}`;
|
|
33
|
+
}
|
|
66
34
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
35
|
+
export async function sendVoice(
|
|
36
|
+
ctx: MiokiContext,
|
|
37
|
+
e: any,
|
|
38
|
+
filePath: string,
|
|
39
|
+
logTag = "[audio]",
|
|
40
|
+
): Promise<void> {
|
|
41
|
+
const fileUrl = filePath.startsWith("file://")
|
|
42
|
+
? filePath
|
|
43
|
+
: `file://${filePath}`;
|
|
44
|
+
try {
|
|
45
|
+
await e.reply([ctx.segment.record(fileUrl)]);
|
|
46
|
+
return;
|
|
47
|
+
} catch (err: any) {
|
|
48
|
+
const msg = String(err?.message ?? err);
|
|
49
|
+
if (
|
|
50
|
+
!/File URL path must be absolute|ENOENT|cannot access|No such file/i.test(
|
|
51
|
+
msg,
|
|
52
|
+
)
|
|
53
|
+
) {
|
|
54
|
+
throw err;
|
|
55
|
+
}
|
|
56
|
+
const localPath = filePath.replace(/^file:\/\//, "");
|
|
57
|
+
const buf = await readFile(localPath);
|
|
58
|
+
const b64 = `base64://${buf.toString("base64")}`;
|
|
59
|
+
ctx.logger.info(
|
|
60
|
+
`${logTag} file:// 发送失败 (${msg}),回退 base64 (${buf.length} bytes)`,
|
|
61
|
+
);
|
|
62
|
+
await e.reply([ctx.segment.record(b64)]);
|
|
70
63
|
}
|
|
71
|
-
|
|
72
|
-
return `base64://${audioBase64}`;
|
|
73
64
|
}
|
package/core/prompt.ts
CHANGED
|
@@ -279,7 +279,7 @@ function buildResponseFormatSection(
|
|
|
279
279
|
- Use [reply:123456] at the START of a line to quote-reply that message (123456 is message_id)
|
|
280
280
|
- **You can use MULTIPLE [reply:xxx] markers in different lines to quote multiple messages!**`);
|
|
281
281
|
|
|
282
|
-
if (ctx.config.audio?.enabled
|
|
282
|
+
if (ctx.config.audio?.enabled) {
|
|
283
283
|
lines.push(`
|
|
284
284
|
### Optional Voice Message Format
|
|
285
285
|
- You MAY optionally send one voice message by writing [audio:content]
|
|
@@ -525,7 +525,7 @@ function buildReviewGuidance(isMulti: boolean, length: Strength): string[] {
|
|
|
525
525
|
function buildPokedGuidance(length: Strength): string[] {
|
|
526
526
|
return [
|
|
527
527
|
"Someone pokes you in a group, probably out of non-malicious play or to draw your attention to what happened in the group chat.",
|
|
528
|
-
"Don't make a fuss about replying, just observe whether the chat history in the group has noteworthy content, and if not, simply say hello or express concern to the user.",
|
|
528
|
+
"Don't make a fuss about replying, just observe whether the chat history in the group has noteworthy content, and if not, simply say hello or express concern to the user. Don't stress about being poked.",
|
|
529
529
|
POKED_LENGTH[length],
|
|
530
530
|
].filter(Boolean);
|
|
531
531
|
}
|
package/handlers/message.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
summarizeHistoryVideo,
|
|
26
26
|
} from "../core/media/history-media";
|
|
27
27
|
import { handleIdleCheckDebug } from "./idle-debug";
|
|
28
|
+
import { handleTtsCommand } from "./tts-command";
|
|
28
29
|
import { processChat } from "../core/chat-turn";
|
|
29
30
|
|
|
30
31
|
const POKE_COOLDOWN_MS = 10 * 60_000;
|
|
@@ -55,6 +56,12 @@ export function createMessageHandler(
|
|
|
55
56
|
return;
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
if (text.startsWith("/tts")) {
|
|
60
|
+
const payload = text.replace(/^\/tts\s*/, "");
|
|
61
|
+
await handleTtsCommand(pluginCtx, e, payload);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
58
65
|
if (text === "/重置会话") {
|
|
59
66
|
if (groupId) {
|
|
60
67
|
pluginCtx.sessionManager.resetBotMessages(`group:${groupId}`);
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type { SupportedLang } from "mioku-service-audio";
|
|
2
|
+
import type { ChatPluginContext } from "../context";
|
|
3
|
+
import { sendVoice } from "../core/media/audio";
|
|
4
|
+
|
|
5
|
+
function detectLang(text: string): SupportedLang {
|
|
6
|
+
if (/[\u3040-\u30ff]/.test(text)) return "ja";
|
|
7
|
+
if (/[\u4e00-\u9fff]/.test(text)) return "zh";
|
|
8
|
+
if (/[\uac00-\ud7af]/.test(text)) return "ko";
|
|
9
|
+
return "en";
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function handleTtsCommand(
|
|
13
|
+
pluginCtx: ChatPluginContext,
|
|
14
|
+
e: any,
|
|
15
|
+
payload: string,
|
|
16
|
+
): Promise<void> {
|
|
17
|
+
const { ctx } = pluginCtx;
|
|
18
|
+
const isOwner = ctx.isOwner?.(e) ?? false;
|
|
19
|
+
if (!isOwner) {
|
|
20
|
+
await e.reply("只有主人才能使用 /tts 指令~");
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const text = String(payload || "").trim();
|
|
25
|
+
if (!text) {
|
|
26
|
+
await e.reply("用法: /tts <要合成的文本>");
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const audioService = pluginCtx.audioService;
|
|
31
|
+
if (!audioService) {
|
|
32
|
+
await e.reply("audio 服务未加载,请安装 mioku-service-audio 后重启");
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const isReady = await audioService.ready().catch(() => false);
|
|
37
|
+
if (!isReady) {
|
|
38
|
+
ctx.logger.warn(
|
|
39
|
+
`[tts-cmd] audio 未就绪 status=${JSON.stringify(audioService.getStatus?.())}`,
|
|
40
|
+
);
|
|
41
|
+
await e.reply("audio 服务正在启动/失败,请稍后再试");
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
ctx.logger.info(`[tts-cmd] 推理: "${text.slice(0, 40)}" lang=${detectLang(text)}`);
|
|
46
|
+
|
|
47
|
+
let result;
|
|
48
|
+
try {
|
|
49
|
+
result = await audioService.generateByText({
|
|
50
|
+
text,
|
|
51
|
+
textLang: detectLang(text),
|
|
52
|
+
mediaType: "wav",
|
|
53
|
+
});
|
|
54
|
+
} catch (err: any) {
|
|
55
|
+
ctx.logger.error(`[tts-cmd] 推理失败: ${err?.message ?? err}`);
|
|
56
|
+
await e.reply(`TTS 推理失败: ${err?.message ?? err}`);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!result?.filePath) {
|
|
61
|
+
await e.reply("TTS 推理未返回音频文件");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
ctx.logger.info(`[tts-cmd] 发送 -> ${result.filePath}`);
|
|
66
|
+
await sendVoice(ctx, e, result.filePath, "[tts-cmd]");
|
|
67
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import { definePlugin, type MiokiContext } from "mioki";
|
|
2
2
|
import type { AIInstance, AIModelRole, AIService } from "mioku";
|
|
3
|
-
import {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
import type { AudioServiceApi } from "mioku-service-audio";
|
|
4
|
+
import { getPluginRuntimeState, getService, Services } from "mioku";
|
|
5
|
+
import type {
|
|
6
|
+
ChatConfig,
|
|
7
|
+
ChatMessage,
|
|
8
|
+
ChatGroupsFile,
|
|
9
|
+
TargetMessage,
|
|
10
|
+
} from "./types";
|
|
9
11
|
import { initDatabase } from "./db";
|
|
10
12
|
import { SessionManager } from "./manage/session";
|
|
11
13
|
import { RateLimiter } from "./manage/rate-limiter";
|
|
@@ -59,14 +61,10 @@ function resolveRoleInstances(aiService: AIService): {
|
|
|
59
61
|
isMultimodal: boolean;
|
|
60
62
|
} | null {
|
|
61
63
|
const getByRole = (role: AIModelRole) =>
|
|
62
|
-
aiService.getInstanceByRole?.(role) ??
|
|
63
|
-
aiService.get?.(role) ??
|
|
64
|
-
undefined;
|
|
64
|
+
aiService.getInstanceByRole?.(role) ?? aiService.get?.(role) ?? undefined;
|
|
65
65
|
|
|
66
66
|
const main =
|
|
67
|
-
getByRole("main") ??
|
|
68
|
-
aiService.getDefault?.() ??
|
|
69
|
-
aiService.get?.("main");
|
|
67
|
+
getByRole("main") ?? aiService.getDefault?.() ?? aiService.get?.("main");
|
|
70
68
|
if (!main) return null;
|
|
71
69
|
|
|
72
70
|
const work = getByRole("working") ?? aiService.get?.("work") ?? main;
|
|
@@ -81,9 +79,13 @@ function resolveRoleInstances(aiService: AIService): {
|
|
|
81
79
|
const findModel = (role: AIModelRole, fallbackInstance: AIInstance) => {
|
|
82
80
|
const full = bindings[role];
|
|
83
81
|
if (full && full.includes("/")) return full.split("/").slice(1).join("/");
|
|
84
|
-
const info = instances.find(
|
|
82
|
+
const info = instances.find(
|
|
83
|
+
(item) => item.role === role || item.name === role,
|
|
84
|
+
);
|
|
85
85
|
if (info?.modelId) return info.modelId;
|
|
86
|
-
const anyInfo = instances.find(
|
|
86
|
+
const anyInfo = instances.find(
|
|
87
|
+
(item) => item.name === (fallbackInstance as any).name,
|
|
88
|
+
);
|
|
87
89
|
return anyInfo?.modelId || "";
|
|
88
90
|
};
|
|
89
91
|
|
|
@@ -121,13 +123,25 @@ export default definePlugin({
|
|
|
121
123
|
const aiService = getService(ctx, Services.AI);
|
|
122
124
|
const configService = getService(ctx, Services.Config);
|
|
123
125
|
const screenshotService = getService(ctx, Services.Screenshot);
|
|
126
|
+
const audioService = ctx.services.audio as AudioServiceApi | undefined;
|
|
124
127
|
let warnedMarkdownScreenshotUnavailable = false;
|
|
128
|
+
if (!audioService) {
|
|
129
|
+
ctx.logger.warn("聊天插件检测到 audio 服务未安装 语音消息将不会发出");
|
|
130
|
+
}
|
|
125
131
|
|
|
126
132
|
if (configService) {
|
|
127
133
|
await configService.registerConfig("chat", "base", BASE_CONFIG);
|
|
128
134
|
await configService.registerConfig("chat", "settings", SETTINGS_CONFIG);
|
|
129
|
-
await configService.registerConfig(
|
|
130
|
-
|
|
135
|
+
await configService.registerConfig(
|
|
136
|
+
"chat",
|
|
137
|
+
"personalization",
|
|
138
|
+
PERSONALIZATION_CONFIG,
|
|
139
|
+
);
|
|
140
|
+
await configService.registerConfig(
|
|
141
|
+
"chat",
|
|
142
|
+
"groups",
|
|
143
|
+
DEFAULT_GROUPS_CONFIG,
|
|
144
|
+
);
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
let cachedBaseConfig: ChatConfig | null = null;
|
|
@@ -150,7 +164,8 @@ export default definePlugin({
|
|
|
150
164
|
);
|
|
151
165
|
}
|
|
152
166
|
const base = (await configService.getConfig("chat", "base")) ?? {};
|
|
153
|
-
const settings =
|
|
167
|
+
const settings =
|
|
168
|
+
(await configService.getConfig("chat", "settings")) ?? {};
|
|
154
169
|
const personalization =
|
|
155
170
|
(await configService.getConfig("chat", "personalization")) ?? {};
|
|
156
171
|
const mergedDefaults = mergeChatConfig(
|
|
@@ -175,20 +190,26 @@ export default definePlugin({
|
|
|
175
190
|
if (!screenshotService && merged.enableMarkdownScreenshot) {
|
|
176
191
|
merged.enableMarkdownScreenshot = false;
|
|
177
192
|
if (!warnedMarkdownScreenshotUnavailable) {
|
|
178
|
-
ctx.logger.warn(
|
|
193
|
+
ctx.logger.warn(
|
|
194
|
+
"聊天插件未加载 screenshot 服务,Markdown 截图渲染已自动关闭",
|
|
195
|
+
);
|
|
179
196
|
warnedMarkdownScreenshotUnavailable = true;
|
|
180
197
|
}
|
|
181
198
|
}
|
|
182
199
|
merged.whitelistGroups = normalizeIdList(merged.whitelistGroups);
|
|
183
200
|
merged.blacklistGroups = normalizeIdList(merged.blacklistGroups);
|
|
184
|
-
merged.mediaAnalysisBlacklistUsers =
|
|
185
|
-
|
|
201
|
+
merged.mediaAnalysisBlacklistUsers = normalizeMediaAnalysisBlacklist(
|
|
202
|
+
merged as Record<string, any>,
|
|
203
|
+
);
|
|
186
204
|
delete (merged as any).imageAnalysisBlacklistUsers;
|
|
187
205
|
|
|
188
206
|
merged.model = roleModels.main || merged.model || "";
|
|
189
|
-
merged.workingModel =
|
|
207
|
+
merged.workingModel =
|
|
208
|
+
roleModels.working || merged.workingModel || merged.model;
|
|
190
209
|
merged.multimodalWorkingModel =
|
|
191
|
-
roleModels.vision ||
|
|
210
|
+
roleModels.vision ||
|
|
211
|
+
merged.multimodalWorkingModel ||
|
|
212
|
+
merged.workingModel;
|
|
192
213
|
merged.isMultimodal = roleIsMultimodal;
|
|
193
214
|
if (!roleIsMultimodal) merged.enableMediaRecognition = false;
|
|
194
215
|
if (!merged.apiKey) merged.apiKey = "__ai_service__";
|
|
@@ -203,7 +224,10 @@ export default definePlugin({
|
|
|
203
224
|
}
|
|
204
225
|
const raw = await configService.getConfig("chat", "groups");
|
|
205
226
|
cachedGroupsConfig =
|
|
206
|
-
raw &&
|
|
227
|
+
raw &&
|
|
228
|
+
typeof raw === "object" &&
|
|
229
|
+
raw.groups &&
|
|
230
|
+
typeof raw.groups === "object"
|
|
207
231
|
? (raw as ChatGroupsFile)
|
|
208
232
|
: DEFAULT_GROUPS_CONFIG;
|
|
209
233
|
};
|
|
@@ -279,7 +303,9 @@ export default definePlugin({
|
|
|
279
303
|
apiUrl: legacyApiUrl,
|
|
280
304
|
apiKey: legacyApiKey,
|
|
281
305
|
modelType: "text",
|
|
282
|
-
model: String(
|
|
306
|
+
model: String(
|
|
307
|
+
(earlyConfig as any).workingModel || legacyModel || "default",
|
|
308
|
+
),
|
|
283
309
|
});
|
|
284
310
|
await aiService.create({
|
|
285
311
|
name: "vision",
|
|
@@ -287,7 +313,9 @@ export default definePlugin({
|
|
|
287
313
|
apiKey: legacyApiKey,
|
|
288
314
|
modelType: "multimodal",
|
|
289
315
|
model: String(
|
|
290
|
-
(earlyConfig as any).multimodalWorkingModel ||
|
|
316
|
+
(earlyConfig as any).multimodalWorkingModel ||
|
|
317
|
+
legacyModel ||
|
|
318
|
+
"default",
|
|
291
319
|
),
|
|
292
320
|
});
|
|
293
321
|
aiService.setDefault("main");
|
|
@@ -367,7 +395,9 @@ export default definePlugin({
|
|
|
367
395
|
const rateLimitGuard = new RateLimitGuard(rateLimiter, ctx.logger);
|
|
368
396
|
const runWithRateLimitGuard = rateLimitGuard.run.bind(rateLimitGuard);
|
|
369
397
|
|
|
370
|
-
const getAIInstance = (
|
|
398
|
+
const getAIInstance = (
|
|
399
|
+
role: AIModelRole = "main",
|
|
400
|
+
): AIInstance | undefined => {
|
|
371
401
|
if (role === "working") {
|
|
372
402
|
return aiService.getInstanceByRole?.("working") ?? workAIInstance;
|
|
373
403
|
}
|
|
@@ -378,12 +408,21 @@ export default definePlugin({
|
|
|
378
408
|
};
|
|
379
409
|
|
|
380
410
|
const pluginCtx = {
|
|
381
|
-
ctx,
|
|
411
|
+
ctx,
|
|
412
|
+
defaultConfig: configProvider(),
|
|
413
|
+
configProvider,
|
|
414
|
+
getConfig,
|
|
382
415
|
db,
|
|
383
|
-
aiInstance: mainAIInstance,
|
|
416
|
+
aiInstance: mainAIInstance,
|
|
417
|
+
workAIInstance,
|
|
418
|
+
visionAIInstance,
|
|
384
419
|
getAIInstance,
|
|
385
|
-
aiService,
|
|
386
|
-
|
|
420
|
+
aiService,
|
|
421
|
+
humanize,
|
|
422
|
+
sessionManager,
|
|
423
|
+
skillManager,
|
|
424
|
+
rateLimiter,
|
|
425
|
+
queueManager,
|
|
387
426
|
groupStructuredHistory,
|
|
388
427
|
cooldownManager: undefined as unknown as CooldownManager,
|
|
389
428
|
idleCheckManager: undefined as unknown as IdleCheckManager,
|
|
@@ -391,28 +430,41 @@ export default definePlugin({
|
|
|
391
430
|
sessionTurnScheduler,
|
|
392
431
|
runWithRateLimitGuard,
|
|
393
432
|
buildHistoryMediaOptions,
|
|
394
|
-
getGroupHistoryMessages,
|
|
395
|
-
|
|
433
|
+
getGroupHistoryMessages,
|
|
434
|
+
getGroupInfoData,
|
|
435
|
+
getHumanizeContexts,
|
|
436
|
+
sendAIResponse,
|
|
437
|
+
sendMessage,
|
|
438
|
+
saveBotMessages,
|
|
439
|
+
sendEmoji,
|
|
396
440
|
buildToolContext,
|
|
397
441
|
buildStructuredUserInputFromTarget,
|
|
398
442
|
runChat,
|
|
443
|
+
audioService,
|
|
399
444
|
startCooldownTimer: (
|
|
400
445
|
groupSessionId: string,
|
|
401
446
|
groupId: number,
|
|
402
447
|
selfId: number,
|
|
403
448
|
) => cooldownManager.startCooldownTimer(groupSessionId, groupId, selfId),
|
|
404
|
-
async recordGroupMessageForLearning(
|
|
449
|
+
async recordGroupMessageForLearning(
|
|
450
|
+
userMsg: ChatMessage,
|
|
451
|
+
groupSessionId: string,
|
|
452
|
+
) {
|
|
405
453
|
db.saveMessage(userMsg);
|
|
406
454
|
await Promise.all([
|
|
407
455
|
humanize.expressionLearner.onMessage(userMsg),
|
|
408
456
|
humanize.topicTracker.onMessage(groupSessionId),
|
|
409
457
|
]);
|
|
410
458
|
if (userMsg.groupId) {
|
|
411
|
-
const ttlMs = configProvider(
|
|
459
|
+
const ttlMs = configProvider(
|
|
460
|
+
userMsg.groupId,
|
|
461
|
+
).groupStructuredHistoryTtlMs;
|
|
412
462
|
groupStructuredHistory.append(
|
|
413
463
|
groupSessionId,
|
|
414
464
|
buildStructuredUserMessages([
|
|
415
|
-
buildStructuredUserInputFromTarget(
|
|
465
|
+
buildStructuredUserInputFromTarget(
|
|
466
|
+
userMsg as unknown as TargetMessage,
|
|
467
|
+
),
|
|
416
468
|
]),
|
|
417
469
|
ttlMs,
|
|
418
470
|
);
|
|
@@ -427,8 +479,14 @@ export default definePlugin({
|
|
|
427
479
|
const queueProcessor = new QueueProcessor(pluginCtx);
|
|
428
480
|
pluginCtx.queueProcessor = queueProcessor;
|
|
429
481
|
|
|
430
|
-
const cleanupInterval = setInterval(
|
|
431
|
-
|
|
482
|
+
const cleanupInterval = setInterval(
|
|
483
|
+
() => skillManager.cleanup(),
|
|
484
|
+
10 * 60_000,
|
|
485
|
+
);
|
|
486
|
+
const dbCleanup = new ChatDatabaseCleanup(
|
|
487
|
+
db,
|
|
488
|
+
configProvider().retention ?? DEFAULT_CLEANUP_CONFIG,
|
|
489
|
+
);
|
|
432
490
|
dbCleanup.start();
|
|
433
491
|
idleCheckManager.start();
|
|
434
492
|
|
|
@@ -440,7 +498,11 @@ export default definePlugin({
|
|
|
440
498
|
},
|
|
441
499
|
matchMessageCommands: (
|
|
442
500
|
getPluginRuntimeState("boot") as
|
|
443
|
-
| {
|
|
501
|
+
| {
|
|
502
|
+
matchMessageCommands?: (
|
|
503
|
+
text: string,
|
|
504
|
+
) => Array<{ plugin: string; command: string }>;
|
|
505
|
+
}
|
|
444
506
|
| undefined
|
|
445
507
|
)?.matchMessageCommands,
|
|
446
508
|
pokeCooldowns,
|
package/manage/types.ts
CHANGED
|
@@ -71,6 +71,7 @@ export type SendMessage = (
|
|
|
71
71
|
text: string,
|
|
72
72
|
config: ChatConfig,
|
|
73
73
|
selfId: number,
|
|
74
|
+
audioService?: import("mioku-service-audio").AudioServiceApi,
|
|
74
75
|
) => Promise<void>;
|
|
75
76
|
|
|
76
77
|
export type BuildToolContext = (options: {
|
|
@@ -87,6 +88,7 @@ export type BuildToolContext = (options: {
|
|
|
87
88
|
pendingImageUrls?: string[];
|
|
88
89
|
humanize: HumanizeEngine;
|
|
89
90
|
targetMessage: TargetMessage;
|
|
91
|
+
audioService?: import("mioku-service-audio").AudioServiceApi;
|
|
90
92
|
}) => ToolContext;
|
|
91
93
|
|
|
92
94
|
export type BuildStructuredUserInput = (event: any, content: string, fallbackTimestamp?: number) => StructuredUserInput;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mioku-plugin-chat",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.9.0",
|
|
4
4
|
"description": "AI 智能聊天插件",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -38,6 +38,11 @@
|
|
|
38
38
|
"cmd": "/重置群会话",
|
|
39
39
|
"desc": "重置当前群的AI聊天记录",
|
|
40
40
|
"role": "admin"
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
"cmd": "/tts <文本>",
|
|
44
|
+
"desc": "TTS 推理",
|
|
45
|
+
"role": "owner"
|
|
41
46
|
}
|
|
42
47
|
]
|
|
43
48
|
}
|
|
@@ -53,7 +58,8 @@
|
|
|
53
58
|
"mioku": "workspace:*"
|
|
54
59
|
},
|
|
55
60
|
"peerDependencies": {
|
|
56
|
-
"mioku": "^0.
|
|
61
|
+
"mioku": "^0.9.0",
|
|
62
|
+
"mioku-service-audio": "workspace:*",
|
|
57
63
|
"mioki": "^0.16.0"
|
|
58
64
|
}
|
|
59
65
|
}
|
package/types.ts
CHANGED