mioku-plugin-music 1.1.0 → 2.0.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/index.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { definePlugin } from "mioki";
2
- import type { AIService } from "../../src/services/ai/types";
3
- import type { ScreenshotService } from "../../src/services/screenshot/types";
4
- import type { ConfigService } from "../../src/services/config/tpyes";
5
- import type { AppleMusicServiceApi } from "../../src/services/applemusic/types";
2
+ import type { AIService } from "mioku";
3
+ import type { ScreenshotService } from "mioku";
4
+ import type { ConfigService } from "mioku";
5
+ import type { AppleMusicServiceApi } from "mioku-service-applemusic";
6
6
  import { resetMusicRuntimeState, setMusicRuntimeState } from "./runtime";
7
7
  import { MusicPluginRuntime } from "./runtime-core/service";
8
8
  import { MUSIC_DEFAULTS } from "./config";
package/package.json CHANGED
@@ -1,8 +1,9 @@
1
1
  {
2
2
  "name": "mioku-plugin-music",
3
- "version": "1.1.0",
3
+ "version": "2.0.0",
4
4
  "description": "音乐插件:点歌、搜索音乐、听歌语音发送",
5
5
  "main": "index.ts",
6
+ "type": "module",
6
7
  "keywords": [
7
8
  "mioku"
8
9
  ],
@@ -40,5 +41,15 @@
40
41
  }
41
42
  ]
42
43
  }
44
+ },
45
+ "peerDependencies": {
46
+ "mioku": "^0.8.0",
47
+ "mioki": "^0.16.0",
48
+ "mioku-service-applemusic": "^2.0.0"
49
+ },
50
+ "devDependencies": {
51
+ "mioku": "workspace:*",
52
+ "mioki": "^0.16.0",
53
+ "mioku-service-applemusic": "workspace:*"
43
54
  }
44
55
  }
@@ -1,4 +1,4 @@
1
- import type { AppleMusicServiceApi } from "../../../src/services/applemusic/types";
1
+ import type { AppleMusicServiceApi } from "mioku-service-applemusic";
2
2
  import type {
3
3
  DownloadSongResult,
4
4
  MusicAlbumDetail,
@@ -1,4 +1,4 @@
1
- import type { AppleMusicServiceApi } from "../../../src/services/applemusic/types";
1
+ import type { AppleMusicServiceApi } from "mioku-service-applemusic";
2
2
  import { AppleMusicProvider } from "./applemusic-provider";
3
3
  import {
4
4
  type MusicProvider,
@@ -1,4 +1,4 @@
1
- import type { AIService } from "../../../src/services/ai/types";
1
+ import type { AIService } from "mioku";
2
2
  import { sendTextMessage } from "./message";
3
3
 
4
4
  function normalizeErrorMessage(error: unknown): string {
@@ -25,6 +25,11 @@ export async function notifyFallback(options: {
25
25
  );
26
26
  }
27
27
 
28
+ const text = options.ctx.text(options.event)?.trim() ?? "";
29
+ if (!text.startsWith("/")) {
30
+ return;
31
+ }
32
+
28
33
  const chatRuntime = options.aiService?.getChatRuntime();
29
34
  if (chatRuntime) {
30
35
  try {
@@ -1,4 +1,5 @@
1
1
  import * as fs from "fs/promises";
2
+ import * as path from "path";
2
3
 
3
4
  function normalizeFileSource(file: string): string {
4
5
  const value = String(file || "").trim();
@@ -100,6 +101,81 @@ export async function sendImageMessage(
100
101
  }
101
102
  }
102
103
 
104
+ export async function sendFileMessage(
105
+ ctx: any,
106
+ event: any,
107
+ filePath: string,
108
+ name: string,
109
+ ): Promise<void> {
110
+ const ext = path.extname(filePath);
111
+ const fileName = ext ? `${name}${ext}` : name;
112
+ const { bot, groupId, userId } = getBotAndTarget(ctx, event);
113
+ const sendPayload = async (source: string) => {
114
+ const payload: any[] = [];
115
+ payload.push(
116
+ ctx?.segment?.file
117
+ ? ctx.segment.file(normalizeFileSource(source), { name: fileName })
118
+ : { type: "file", file: normalizeFileSource(source), name: fileName },
119
+ );
120
+
121
+ if (bot && groupId != null) {
122
+ await bot.sendGroupMsg(groupId, payload);
123
+ return;
124
+ }
125
+ if (bot && userId != null) {
126
+ await bot.sendPrivateMsg(userId, payload);
127
+ return;
128
+ }
129
+ if (typeof event?.reply === "function") {
130
+ await event.reply(payload);
131
+ return;
132
+ }
133
+ throw new Error("当前上下文不支持文件发送");
134
+ };
135
+
136
+ const canReadLocalFile =
137
+ filePath.startsWith("/") || /^[A-Za-z]:[\\/]/.test(filePath);
138
+
139
+ let fileSendError: unknown;
140
+ try {
141
+ await sendPayload(filePath);
142
+ return;
143
+ } catch (error) {
144
+ fileSendError = error;
145
+ }
146
+
147
+ if (!canReadLocalFile) {
148
+ throw fileSendError instanceof Error
149
+ ? fileSendError
150
+ : new Error(String(fileSendError || "文件发送失败"));
151
+ }
152
+
153
+ let buffer: Buffer;
154
+ try {
155
+ buffer = await fs.readFile(filePath);
156
+ } catch {
157
+ throw fileSendError instanceof Error
158
+ ? fileSendError
159
+ : new Error(String(fileSendError || "文件发送失败"));
160
+ }
161
+
162
+ const base64 = `base64://${buffer.toString("base64")}`;
163
+ try {
164
+ await sendPayload(base64);
165
+ } catch (base64Error) {
166
+ const message = String(base64Error || "");
167
+ const timeoutLike =
168
+ message.includes("timeout") ||
169
+ message.includes("超时") ||
170
+ message.includes("timed out") ||
171
+ message.includes("ETIMEDOUT");
172
+ if (timeoutLike) {
173
+ return;
174
+ }
175
+ throw new Error("文件发送失败:路径发送失败,base64 发送也失败");
176
+ }
177
+ }
178
+
103
179
  export async function sendRecordMessage(
104
180
  ctx: any,
105
181
  event: any,
@@ -1,6 +1,6 @@
1
- import type { AIService } from "../../../src/services/ai/types";
2
- import type { ScreenshotService } from "../../../src/services/screenshot/types";
3
- import type { AppleMusicServiceApi } from "../../../src/services/applemusic/types";
1
+ import type { AIService } from "mioku";
2
+ import type { ScreenshotService } from "mioku";
3
+ import type { AppleMusicServiceApi } from "mioku-service-applemusic";
4
4
  import { MUSIC_DEFAULTS } from "../config";
5
5
  import {
6
6
  createMusicProvider,
@@ -14,6 +14,7 @@ import {
14
14
  sendImageMessage,
15
15
  sendRecordMessage,
16
16
  sendTextMessage,
17
+ sendFileMessage,
17
18
  } from "./message";
18
19
  import type {
19
20
  MusicBaseConfig,
@@ -170,7 +171,12 @@ export class MusicPluginRuntime {
170
171
  return provider.getAlbumDetail(albumId);
171
172
  }
172
173
 
173
- async sendSongById(ctx: any, event: any, songId: string): Promise<void> {
174
+ async sendSongById(
175
+ ctx: any,
176
+ event: any,
177
+ songId: string,
178
+ fileName?: string,
179
+ ): Promise<void> {
174
180
  const session = this.getOrCreateSession(event);
175
181
  const provider = this.createProvider(
176
182
  session.provider,
@@ -180,10 +186,22 @@ export class MusicPluginRuntime {
180
186
  this.deps.logger.info(
181
187
  `[music] download songId=${songId} source=${result.sourceType} file=${result.filePath}`,
182
188
  );
189
+ if (!result || typeof result !== "object") {
190
+ throw new Error(`downloadSong 返回异常: ${JSON.stringify(result)}`);
191
+ }
192
+ if ("tracks" in result && Array.isArray((result as any).tracks)) {
193
+ this.deps.logger.warn(`[music] downloadSong 返回了 tracks 字段`);
194
+ }
183
195
  if (result.sourceType !== "hls") {
184
196
  throw new Error("当前未获取到高质量 HLS 音频,请检查 media user token");
185
197
  }
186
- await sendRecordMessage(ctx, event, result.filePath);
198
+ const isPrivate = event?.message_type !== "group";
199
+ if (isPrivate) {
200
+ const finalName = fileName || "song";
201
+ await sendFileMessage(ctx, event, result.filePath, finalName);
202
+ } else {
203
+ await sendRecordMessage(ctx, event, result.filePath);
204
+ }
187
205
  }
188
206
 
189
207
  async notifyFailure(
@@ -209,13 +227,13 @@ export class MusicPluginRuntime {
209
227
  ctx,
210
228
  event,
211
229
  aiService: this.deps.aiService,
212
- instruction: `用户说“听${query}”,但搜索不到可播放歌曲。请简短提示换关键词。`,
230
+ instruction: `用户说”听${query}”,但搜索不到可播放歌曲。请简短提示换关键词。`,
213
231
  fallbackMessage: `没有找到和「${query}」相关的歌曲`,
214
232
  });
215
233
  return;
216
234
  }
217
235
 
218
- await this.sendSongById(ctx, event, first.id);
236
+ await this.sendSongById(ctx, event, first.id, first.title);
219
237
  }
220
238
 
221
239
  private async searchAndSendList(
@@ -240,7 +258,7 @@ export class MusicPluginRuntime {
240
258
  await sendTextMessage(
241
259
  ctx,
242
260
  event,
243
- `已找到 ${result.tracks.length} 首。发送“听1”播放第一首。`,
261
+ `已找到 ${result.tracks.length} 首。发送”听1”播放第一首。`,
244
262
  );
245
263
  return;
246
264
  }
@@ -255,7 +273,7 @@ export class MusicPluginRuntime {
255
273
  });
256
274
  await sendImageMessage(ctx, event, imagePath);
257
275
  this.deps.logger.info(
258
- `[music] search rendered query="${keyword}" count=${result.tracks.length}`,
276
+ `[music] search rendered query=”${keyword} count=${result.tracks.length}`,
259
277
  );
260
278
  } catch (error) {
261
279
  await notifyFallback({
@@ -283,20 +301,20 @@ export class MusicPluginRuntime {
283
301
  event,
284
302
  aiService: this.deps.aiService,
285
303
  instruction: `用户请求听第${index}首,但当前列表不足。请提示先点歌或检查编号。`,
286
- fallbackMessage: `没有第 ${index} 首,请先“点歌 关键词”再选择`,
304
+ fallbackMessage: `没有第 ${index} 首,请先”点歌 关键词”再选择`,
287
305
  });
288
306
  return;
289
307
  }
290
308
 
291
309
  try {
292
- await this.sendSongById(ctx, event, target.id);
310
+ await this.sendSongById(ctx, event, target.id, target.title);
293
311
  } catch (error) {
294
312
  await notifyFallback({
295
313
  ctx,
296
314
  event,
297
315
  aiService: this.deps.aiService,
298
316
  instruction: `用户选择听第${index}首时下载失败。错误:${String(error)}。请简短道歉并建议重试。`,
299
- fallbackMessage: "播放失败,请稍后重试。",
317
+ fallbackMessage: "播放失败,请稍后重试",
300
318
  error,
301
319
  });
302
320
  }
@@ -314,8 +332,8 @@ export class MusicPluginRuntime {
314
332
  ctx,
315
333
  event,
316
334
  aiService: this.deps.aiService,
317
- instruction: `用户请求“听${keyword}”时失败。错误:${String(error)}。请简短提示后重试。`,
318
- fallbackMessage: "播放失败,请稍后重试。",
335
+ instruction: `用户请求”听${keyword}”时失败。错误:${String(error)}。请简短提示后重试。`,
336
+ fallbackMessage: "播放失败,请稍后重试",
319
337
  error,
320
338
  });
321
339
  }
@@ -384,6 +402,10 @@ export class MusicPluginRuntime {
384
402
  }
385
403
 
386
404
  private async tryReactToCommandMessage(ctx: any, event: any): Promise<void> {
405
+ if (event?.message_type === "group") {
406
+ return;
407
+ }
408
+
387
409
  const messageId = Number(event?.message_id);
388
410
  if (!Number.isFinite(messageId) || messageId <= 0) {
389
411
  return;
package/runtime.ts CHANGED
@@ -3,7 +3,7 @@ import {
3
3
  getPluginRuntimeState,
4
4
  resetPluginRuntimeState,
5
5
  setPluginRuntimeState,
6
- } from "../../src";
6
+ } from "mioku";
7
7
 
8
8
  export interface MusicRuntimeState {
9
9
  runtime?: MusicPluginRuntime;
package/skills.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AISkill, AITool } from "../../src";
1
+ import type { AISkill, AITool } from "mioku";
2
2
  import { getMusicRuntimeState } from "./runtime";
3
3
 
4
4
  const musicSkills: AISkill[] = [
@@ -76,7 +76,8 @@ const musicSkills: AISkill[] = [
76
76
  required: [],
77
77
  },
78
78
  handler: async (args: any, runtimeCtx?: any) => {
79
- const runtime = getMusicRuntimeState().runtime;
79
+ const runtimeState = getMusicRuntimeState();
80
+ const runtime = runtimeState.runtime;
80
81
  const ctx = runtimeCtx?.ctx;
81
82
  const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
82
83
  if (!runtime || !ctx || !event) {
@@ -89,6 +90,8 @@ const musicSkills: AISkill[] = [
89
90
  return "必须提供 song_id 或 query";
90
91
  }
91
92
 
93
+ ctx.logger.info(`[music] send_music_song called: songId=${songId}, query=${query}`);
94
+
92
95
  try {
93
96
  if (songId) {
94
97
  await runtime.sendSongById(ctx, event, songId);
@@ -98,6 +101,7 @@ const musicSkills: AISkill[] = [
98
101
  await runtime.sendSongByQuery(ctx, event, query);
99
102
  return "已按关键词发送歌曲";
100
103
  } catch (error) {
104
+ ctx.logger.error(`[music] send_music_song failed: ${error}`);
101
105
  await runtime.notifyFailure(
102
106
  ctx,
103
107
  event,