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 +4 -4
- package/package.json +12 -1
- package/providers/applemusic-provider.ts +1 -1
- package/providers/factory.ts +1 -1
- package/runtime-core/fallback.ts +6 -1
- package/runtime-core/message.ts +76 -0
- package/runtime-core/service.ts +36 -14
- package/runtime.ts +1 -1
- package/skills.ts +6 -2
package/index.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { definePlugin } from "mioki";
|
|
2
|
-
import type { AIService } from "
|
|
3
|
-
import type { ScreenshotService } from "
|
|
4
|
-
import type { ConfigService } from "
|
|
5
|
-
import type { AppleMusicServiceApi } from "
|
|
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": "
|
|
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
|
}
|
package/providers/factory.ts
CHANGED
package/runtime-core/fallback.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AIService } from "
|
|
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 {
|
package/runtime-core/message.ts
CHANGED
|
@@ -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,
|
package/runtime-core/service.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import type { AIService } from "
|
|
2
|
-
import type { ScreenshotService } from "
|
|
3
|
-
import type { AppleMusicServiceApi } from "
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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}
|
|
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
|
|
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:
|
|
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
package/skills.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AISkill, AITool } from "
|
|
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
|
|
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,
|