mioku-plugin-music 2.0.0 → 2.2.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/README.md CHANGED
@@ -4,8 +4,8 @@
4
4
 
5
5
  - 点歌搜索:`点歌 晴天`
6
6
  - 搜索结果截图列表
7
- - 编号听歌:`听1`
8
- - 关键词直听:`听晴天`
7
+ - 编号听歌(语音):`听1`
8
+ - 编号听歌(原曲文件,群聊也直接发文件):`原曲1`
9
9
  - AI skills:搜索、发送歌曲
10
10
 
11
11
  ## 已适配 Provider
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-music",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "音乐插件:点歌、搜索音乐、听歌语音发送",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -17,6 +17,20 @@
17
17
  "ai",
18
18
  "screenshot"
19
19
  ],
20
+ "accessHooks": [
21
+ {
22
+ "id": "点歌",
23
+ "match": "/^\\/?点歌\\s+/"
24
+ },
25
+ {
26
+ "id": "听",
27
+ "match": "/^听\\d+$/"
28
+ },
29
+ {
30
+ "id": "原曲",
31
+ "match": "/^原曲\\d+$/"
32
+ }
33
+ ],
20
34
  "help": {
21
35
  "title": "音乐",
22
36
  "description": "点歌并发送语音,支持搜索结果图片列表",
@@ -34,9 +48,9 @@
34
48
  "role": "member"
35
49
  },
36
50
  {
37
- "cmd": " <关键词>",
38
- "desc": "直接按关键词搜索并发送第一首歌曲语音",
39
- "usage": "听晴天",
51
+ "cmd": "原曲 <编号>",
52
+ "desc": "发送上次搜索列表中的指定歌曲原曲文件",
53
+ "usage": "原曲1",
40
54
  "role": "member"
41
55
  }
42
56
  ]
@@ -66,7 +66,7 @@ export function renderMusicSearchListHtml(search: MusicSearchResult): string {
66
66
  <div class="grid gap-3">${items}</div>
67
67
  <div class="mt-5 flex items-center justify-between text-[16px] text-slate-600 dark:text-slate-300">
68
68
  <div>音源:${escapeHtml(providerLabel)}</div>
69
- <div>发送「听1」播放第一首歌曲</div>
69
+ <div>发送「听1」或「原曲1」试试看吧~</div>
70
70
  </div>
71
71
  </div>
72
72
  `;
@@ -42,25 +42,25 @@ function parseListenIndex(text: string): number | null {
42
42
  return idx;
43
43
  }
44
44
 
45
- function parseSearchKeyword(text: string): string | null {
46
- const match = text.match(/^\/?点歌\s*(.+)$/);
45
+ function parseOriginalIndex(text: string): number | null {
46
+ const match = text.match(/^\/?原曲\s*(\d{1,2})$/);
47
47
  if (!match) {
48
48
  return null;
49
49
  }
50
- const value = String(match[1] || "").trim();
51
- return value ? value : null;
50
+ const idx = Number(match[1]);
51
+ if (!Number.isFinite(idx) || idx <= 0) {
52
+ return null;
53
+ }
54
+ return idx;
52
55
  }
53
56
 
54
- function parseListenKeyword(text: string): string | null {
55
- const match = text.match(/^\/?听\s*(.+)$/);
57
+ function parseSearchKeyword(text: string): string | null {
58
+ const match = text.match(/^\/?点歌\s*(.+)$/);
56
59
  if (!match) {
57
60
  return null;
58
61
  }
59
62
  const value = String(match[1] || "").trim();
60
- if (!value || /^\d+$/.test(value)) {
61
- return null;
62
- }
63
- return value;
63
+ return value ? value : null;
64
64
  }
65
65
 
66
66
  export class MusicPluginRuntime {
@@ -124,14 +124,14 @@ export class MusicPluginRuntime {
124
124
  const listenIndex = parseListenIndex(text);
125
125
  if (listenIndex != null) {
126
126
  await this.tryReactToCommandMessage(ctx, event);
127
- await this.sendByIndex(ctx, event, listenIndex);
127
+ await this.sendByIndex(ctx, event, listenIndex, false);
128
128
  return true;
129
129
  }
130
130
 
131
- const listenKeyword = parseListenKeyword(text);
132
- if (listenKeyword) {
131
+ const originalIndex = parseOriginalIndex(text);
132
+ if (originalIndex != null) {
133
133
  await this.tryReactToCommandMessage(ctx, event);
134
- await this.sendByKeyword(ctx, event, listenKeyword);
134
+ await this.sendByIndex(ctx, event, originalIndex, true);
135
135
  return true;
136
136
  }
137
137
 
@@ -176,6 +176,7 @@ export class MusicPluginRuntime {
176
176
  event: any,
177
177
  songId: string,
178
178
  fileName?: string,
179
+ options?: { forceFile?: boolean },
179
180
  ): Promise<void> {
180
181
  const session = this.getOrCreateSession(event);
181
182
  const provider = this.createProvider(
@@ -196,7 +197,8 @@ export class MusicPluginRuntime {
196
197
  throw new Error("当前未获取到高质量 HLS 音频,请检查 media user token");
197
198
  }
198
199
  const isPrivate = event?.message_type !== "group";
199
- if (isPrivate) {
200
+ const sendAsFile = isPrivate || options?.forceFile === true;
201
+ if (sendAsFile) {
200
202
  const finalName = fileName || "song";
201
203
  await sendFileMessage(ctx, event, result.filePath, finalName);
202
204
  } else {
@@ -219,7 +221,12 @@ export class MusicPluginRuntime {
219
221
  });
220
222
  }
221
223
 
222
- async sendSongByQuery(ctx: any, event: any, query: string): Promise<void> {
224
+ async sendSongByQuery(
225
+ ctx: any,
226
+ event: any,
227
+ query: string,
228
+ options?: { forceFile?: boolean },
229
+ ): Promise<void> {
223
230
  const result = await this.searchSongs(event, query);
224
231
  const first = result.tracks[0];
225
232
  if (!first) {
@@ -233,7 +240,7 @@ export class MusicPluginRuntime {
233
240
  return;
234
241
  }
235
242
 
236
- await this.sendSongById(ctx, event, first.id, first.title);
243
+ await this.sendSongById(ctx, event, first.id, first.title, options);
237
244
  }
238
245
 
239
246
  private async searchAndSendList(
@@ -291,6 +298,7 @@ export class MusicPluginRuntime {
291
298
  ctx: any,
292
299
  event: any,
293
300
  index: number,
301
+ forceFile: boolean,
294
302
  ): Promise<void> {
295
303
  const session = this.getOrCreateSession(event);
296
304
  const tracks = session.lastSearch?.tracks || [];
@@ -307,32 +315,15 @@ export class MusicPluginRuntime {
307
315
  }
308
316
 
309
317
  try {
310
- await this.sendSongById(ctx, event, target.id, target.title);
311
- } catch (error) {
312
- await notifyFallback({
313
- ctx,
314
- event,
315
- aiService: this.deps.aiService,
316
- instruction: `用户选择听第${index}首时下载失败。错误:${String(error)}。请简短道歉并建议重试。`,
317
- fallbackMessage: "播放失败,请稍后重试",
318
- error,
318
+ await this.sendSongById(ctx, event, target.id, target.title, {
319
+ forceFile,
319
320
  });
320
- }
321
- }
322
-
323
- private async sendByKeyword(
324
- ctx: any,
325
- event: any,
326
- keyword: string,
327
- ): Promise<void> {
328
- try {
329
- await this.sendSongByQuery(ctx, event, keyword);
330
321
  } catch (error) {
331
322
  await notifyFallback({
332
323
  ctx,
333
324
  event,
334
325
  aiService: this.deps.aiService,
335
- instruction: `用户请求”听${keyword}”时失败。错误:${String(error)}。请简短提示后重试。`,
326
+ instruction: `用户选择听第${index}首时下载失败。错误:${String(error)}。请简短道歉并建议重试。`,
336
327
  fallbackMessage: "播放失败,请稍后重试",
337
328
  error,
338
329
  });
package/skills.ts CHANGED
@@ -60,7 +60,8 @@ const musicSkills: AISkill[] = [
60
60
  } as AITool,
61
61
  {
62
62
  name: "send_music_song",
63
- description: "发送歌曲语音(通过 song_id 或 query)",
63
+ description:
64
+ "发送歌曲(通过 song_id 或 query)。默认发语音;传 as_file=true 时直接发源文件",
64
65
  parameters: {
65
66
  type: "object",
66
67
  properties: {
@@ -72,6 +73,11 @@ const musicSkills: AISkill[] = [
72
73
  type: "string",
73
74
  description: "关键词,内部会搜索后发送第一首",
74
75
  },
76
+ as_file: {
77
+ type: "boolean",
78
+ description: "是否直接发送源文件",
79
+ default: false,
80
+ },
75
81
  },
76
82
  required: [],
77
83
  },
@@ -86,20 +92,27 @@ const musicSkills: AISkill[] = [
86
92
 
87
93
  const songId = String(args?.song_id || "").trim();
88
94
  const query = String(args?.query || "").trim();
95
+ const asFile = args?.as_file === true;
89
96
  if (!songId && !query) {
90
97
  return "必须提供 song_id 或 query";
91
98
  }
92
99
 
93
- ctx.logger.info(`[music] send_music_song called: songId=${songId}, query=${query}`);
100
+ ctx.logger.info(
101
+ `[music] send_music_song called: songId=${songId}, query=${query}, asFile=${asFile}`,
102
+ );
94
103
 
95
104
  try {
96
105
  if (songId) {
97
- await runtime.sendSongById(ctx, event, songId);
98
- return "已发送歌曲";
106
+ await runtime.sendSongById(ctx, event, songId, undefined, {
107
+ forceFile: asFile,
108
+ });
109
+ return asFile ? "已发送歌曲源文件" : "已发送歌曲";
99
110
  }
100
111
 
101
- await runtime.sendSongByQuery(ctx, event, query);
102
- return "已按关键词发送歌曲";
112
+ await runtime.sendSongByQuery(ctx, event, query, {
113
+ forceFile: asFile,
114
+ });
115
+ return asFile ? "已按关键词发送歌曲源文件" : "已按关键词发送歌曲";
103
116
  } catch (error) {
104
117
  ctx.logger.error(`[music] send_music_song failed: ${error}`);
105
118
  await runtime.notifyFailure(