mioku-plugin-chat 2.8.3 → 2.9.9

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.
@@ -18,9 +18,6 @@ export const SETTINGS_CONFIG = {
18
18
  },
19
19
  audio: {
20
20
  enabled: true,
21
- baseUrl: "http://localhost:3939",
22
- apiKey: "fufu",
23
- timeoutMs: 40_000,
24
21
  },
25
22
  blacklistGroups: [],
26
23
  whitelistGroups: [],
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 { synthesizeAudioBase64 } from "./media/audio";
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 || !options.config.audio.baseUrl?.trim()) {
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 synthesizeAudioBase64(options.config.audio, trimmed);
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 {
@@ -312,6 +312,7 @@ export async function processChat(
312
312
  humanize: pluginCtx.humanize,
313
313
  targetMessage,
314
314
  selfId: e.self_id,
315
+ audioService: pluginCtx.audioService,
315
316
  });
316
317
 
317
318
  const result = await pluginCtx.runWithRateLimitGuard(
@@ -497,6 +498,7 @@ async function executeChatRuntimeRequestNow(
497
498
  humanize: pluginCtx.humanize,
498
499
  targetMessage,
499
500
  selfId,
501
+ audioService: pluginCtx.audioService,
500
502
  });
501
503
 
502
504
  if (options.send === false) {
@@ -1,73 +1,64 @@
1
- import type { AudioConfig } from "../../types";
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
- interface TTSResponse {
4
- audio_base64?: string;
5
- detail?: string;
6
- error?: string;
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 synthesizeAudioBase64(
24
- config: AudioConfig,
12
+ export async function synthesizeAudioSource(
13
+ audioService: AudioServiceApi | undefined,
25
14
  text: string,
26
- ): Promise<string> {
27
- const trimmedText = String(text || "").trim();
28
- if (!config.enabled) {
29
- throw new Error("Audio output is disabled");
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 apiKey = config.apiKey?.trim();
44
- if (apiKey) {
45
- headers["X-API-Key"] = apiKey;
46
- }
20
+ const isReady = await audioService.ready().catch(() => false);
21
+ if (!isReady) return null;
47
22
 
48
- const response = await fetch(endpoint, {
49
- method: "POST",
50
- headers,
51
- body: JSON.stringify({
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
- const payload = (await response.json().catch(() => ({}))) as TTSResponse;
59
- if (!response.ok) {
60
- throw new Error(
61
- payload?.detail ||
62
- payload?.error ||
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
- const audioBase64 = String(payload.audio_base64 || "").trim();
68
- if (!audioBase64) {
69
- throw new Error("TTS response missing audio_base64");
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 && ctx.config.audio.baseUrl?.trim()) {
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
  }
@@ -81,6 +81,7 @@ export async function handleIdleCheckDebug(
81
81
  humanize: pluginCtx.humanize,
82
82
  targetMessage,
83
83
  selfId: e.self_id,
84
+ audioService: pluginCtx.audioService,
84
85
  });
85
86
 
86
87
  const result = await pluginCtx.runWithRateLimitGuard(
@@ -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}`);
package/handlers/poke.ts CHANGED
@@ -86,6 +86,7 @@ export function createPokeHandler(
86
86
  humanize: pluginCtx.humanize,
87
87
  targetMessage,
88
88
  selfId: e.self_id,
89
+ audioService: pluginCtx.audioService,
89
90
  });
90
91
 
91
92
  const result = await pluginCtx.runWithRateLimitGuard(
@@ -0,0 +1,69 @@
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(
46
+ `[tts-cmd] 推理: "${text.slice(0, 40)}" lang=${detectLang(text)}`,
47
+ );
48
+
49
+ let result;
50
+ try {
51
+ result = await audioService.generateByText({
52
+ text,
53
+ textLang: detectLang(text),
54
+ mediaType: "wav",
55
+ });
56
+ } catch (err: any) {
57
+ ctx.logger.error(`[tts-cmd] 推理失败: ${err?.message ?? err}`);
58
+ await e.reply(`TTS 推理失败: ${err?.message ?? err}`);
59
+ return;
60
+ }
61
+
62
+ if (!result?.filePath) {
63
+ await e.reply("TTS 推理未返回音频文件");
64
+ return;
65
+ }
66
+
67
+ ctx.logger.info(`[tts-cmd] 发送 -> ${result.filePath}`);
68
+ await sendVoice(ctx, e, result.filePath, "[tts-cmd]");
69
+ }
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
- getPluginRuntimeState,
5
- getService,
6
- Services,
7
- } from "mioku";
8
- import type { ChatConfig, ChatMessage, ChatGroupsFile, TargetMessage } from "./types";
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((item) => item.role === role || item.name === role);
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((item) => item.name === (fallbackInstance as any).name);
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("chat", "personalization", PERSONALIZATION_CONFIG);
130
- await configService.registerConfig("chat", "groups", DEFAULT_GROUPS_CONFIG);
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 = (await configService.getConfig("chat", "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("聊天插件未加载 screenshot 服务,Markdown 截图渲染已自动关闭");
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
- normalizeMediaAnalysisBlacklist(merged as Record<string, any>);
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 = roleModels.working || merged.workingModel || merged.model;
207
+ merged.workingModel =
208
+ roleModels.working || merged.workingModel || merged.model;
190
209
  merged.multimodalWorkingModel =
191
- roleModels.vision || merged.multimodalWorkingModel || merged.workingModel;
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 && typeof raw === "object" && raw.groups && typeof raw.groups === "object"
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((earlyConfig as any).workingModel || legacyModel || "default"),
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 || legacyModel || "default",
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 = (role: AIModelRole = "main"): AIInstance | undefined => {
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, defaultConfig: configProvider(), configProvider, getConfig,
411
+ ctx,
412
+ defaultConfig: configProvider(),
413
+ configProvider,
414
+ getConfig,
382
415
  db,
383
- aiInstance: mainAIInstance, workAIInstance, visionAIInstance,
416
+ aiInstance: mainAIInstance,
417
+ workAIInstance,
418
+ visionAIInstance,
384
419
  getAIInstance,
385
- aiService, humanize,
386
- sessionManager, skillManager, rateLimiter, queueManager,
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, getGroupInfoData, getHumanizeContexts,
395
- sendAIResponse, sendMessage, saveBotMessages, sendEmoji,
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(userMsg: ChatMessage, groupSessionId: string) {
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(userMsg.groupId).groupStructuredHistoryTtlMs;
459
+ const ttlMs = configProvider(
460
+ userMsg.groupId,
461
+ ).groupStructuredHistoryTtlMs;
412
462
  groupStructuredHistory.append(
413
463
  groupSessionId,
414
464
  buildStructuredUserMessages([
415
- buildStructuredUserInputFromTarget(userMsg as unknown as TargetMessage),
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(() => skillManager.cleanup(), 10 * 60_000);
431
- const dbCleanup = new ChatDatabaseCleanup(db, configProvider().retention ?? DEFAULT_CLEANUP_CONFIG);
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
- | { matchMessageCommands?: (text: string) => Array<{ plugin: string; command: string }> }
501
+ | {
502
+ matchMessageCommands?: (
503
+ text: string,
504
+ ) => Array<{ plugin: string; command: string }>;
505
+ }
444
506
  | undefined
445
507
  )?.matchMessageCommands,
446
508
  pokeCooldowns,
@@ -227,6 +227,7 @@ export class CooldownManager {
227
227
  humanize: this.humanize,
228
228
  targetMessage,
229
229
  selfId,
230
+ audioService: this.pluginCtx.audioService,
230
231
  });
231
232
 
232
233
  const contexts = await this.getHumanizeContexts(
@@ -353,6 +354,7 @@ export class CooldownManager {
353
354
  humanize: this.humanize,
354
355
  targetMessage,
355
356
  selfId,
357
+ audioService: this.pluginCtx.audioService,
356
358
  });
357
359
  const { groupName, memberCount } = await this.getGroupInfoData(
358
360
  this.ctx,
@@ -254,6 +254,7 @@ export class IdleCheckManager {
254
254
  humanize: this.humanize,
255
255
  targetMessage,
256
256
  selfId,
257
+ audioService: this.pluginCtx.audioService,
257
258
  });
258
259
 
259
260
  const result = await this.runWithRateLimitGuard(
@@ -184,6 +184,7 @@ export class QueueProcessor {
184
184
  const toolCtx = this.buildToolContext({
185
185
  ctx: this.ctx, event: firstMsg.event, groupSessionId, groupId, userId: firstMsg.userId,
186
186
  config: cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
187
+ audioService: this.pluginCtx.audioService,
187
188
  });
188
189
 
189
190
  this.sessionManager.getOrCreate(groupSessionId, "group", groupId);
@@ -264,6 +265,7 @@ export class QueueProcessor {
264
265
  const toolCtx = this.buildToolContext({
265
266
  ctx: this.ctx, event: null, groupSessionId, groupId, userId: targetMessage.userId,
266
267
  config: cfg, aiService: this.aiService, db: this.db, botRole, humanize: this.humanize, targetMessage, selfId,
268
+ audioService: this.pluginCtx.audioService,
267
269
  });
268
270
 
269
271
  const { history } = await this.getGroupHistoryMessages(groupId, groupSessionId, this.ctx, cfg.historyCount, this.db, selfId, this.buildHistoryMediaOptions(this.aiInstance, cfg));
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.8.3",
3
+ "version": "2.9.9",
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.8.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
@@ -154,11 +154,6 @@ export interface WebReaderConfig {
154
154
  */
155
155
  export interface AudioConfig {
156
156
  enabled: boolean;
157
- // 服务器URL
158
- baseUrl: string;
159
- // 密钥
160
- apiKey: string;
161
- timeoutMs: number;
162
157
  }
163
158
 
164
159
  export interface AIRequestLimitConfig {