mioku-plugin-music 2.4.3 → 3.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,13 +1,11 @@
1
- import { definePlugin } from "mioki";
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
- import type { NeteaseServiceApi } from "mioku-service-netease";
7
- import { resetMusicRuntimeState, setMusicRuntimeState } from "./runtime";
1
+ import { definePlugin } from "mioku";
2
+ import { AppleMusicService } from "mioku-service-applemusic";
3
+ import { NeteaseService } from "mioku-service-netease";
4
+ import { getService, Services } from "mioku";
8
5
  import { MusicPluginRuntime } from "./runtime-core/service";
9
6
  import { MUSIC_DEFAULTS } from "./config";
10
7
  import type { MusicBaseConfig } from "./types";
8
+ import { createMusicSkills } from "./skills/music";
11
9
 
12
10
  function cloneConfig<T>(value: T): T {
13
11
  return JSON.parse(JSON.stringify(value)) as T;
@@ -18,17 +16,11 @@ export default definePlugin({
18
16
  version: "1.0.0",
19
17
  description: "点歌与听歌插件",
20
18
  async setup(ctx) {
21
- const configService = ctx.services?.config as ConfigService | undefined;
22
- const aiService = ctx.services?.ai as AIService | undefined;
23
- const screenshotService = ctx.services?.screenshot as
24
- | ScreenshotService
25
- | undefined;
26
- const applemusicService = ctx.services?.applemusic as
27
- | AppleMusicServiceApi
28
- | undefined;
29
- const neteaseService = ctx.services?.netease as
30
- | NeteaseServiceApi
31
- | undefined;
19
+ const configService = getService(ctx, Services.Config);
20
+ const aiService = getService(ctx, Services.AI);
21
+ const screenshotService = getService(ctx, Services.Screenshot);
22
+ const applemusicService = getService(ctx, AppleMusicService);
23
+ const neteaseService = getService(ctx, NeteaseService);
32
24
  let baseConfig = cloneConfig(MUSIC_DEFAULTS);
33
25
 
34
26
  if (configService) {
@@ -50,7 +42,9 @@ export default definePlugin({
50
42
  });
51
43
  runtime.updateConfig(baseConfig);
52
44
 
53
- setMusicRuntimeState({ runtime });
45
+ if (aiService) {
46
+ for (const skill of createMusicSkills(runtime)) aiService.registerSkill(skill);
47
+ }
54
48
 
55
49
  const disposers: Array<() => void> = [];
56
50
  if (configService) {
@@ -62,15 +56,13 @@ export default definePlugin({
62
56
  );
63
57
  }
64
58
 
65
- ctx.handle("message", async (event: any) => {
59
+ ctx.handle("message", async (event) => {
66
60
  await runtime.handleMessage(ctx, event);
67
61
  });
68
62
 
69
63
  return () => {
70
- for (const dispose of disposers) {
71
- dispose();
72
- }
73
- resetMusicRuntimeState();
64
+ for (const dispose of disposers) dispose();
65
+ if (aiService) aiService.removeSkill("music");
74
66
  };
75
67
  },
76
68
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mioku-plugin-music",
3
- "version": "2.4.3",
3
+ "version": "3.0.0",
4
4
  "description": "音乐插件:点歌、搜索音乐、听歌语音发送",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -57,14 +57,12 @@
57
57
  }
58
58
  },
59
59
  "peerDependencies": {
60
- "mioku": "^0.8.0",
61
- "mioki": "^0.16.0",
62
- "mioku-service-applemusic": "^2.0.0",
63
- "mioku-service-netease": "^1.0.0"
60
+ "mioku": "^1.0.0",
61
+ "mioku-service-applemusic": "^3.0.0",
62
+ "mioku-service-netease": "^2.0.0"
64
63
  },
65
64
  "devDependencies": {
66
65
  "mioku": "workspace:*",
67
- "mioki": "^0.16.0",
68
66
  "mioku-service-applemusic": "workspace:*",
69
67
  "mioku-service-netease": "workspace:*"
70
68
  }
@@ -4,11 +4,11 @@ import { getMusicProviderLabel } from "../providers/provider-labels";
4
4
  function escapeHtml(value: string): string {
5
5
  const source = String(value || "");
6
6
  return source
7
- .replace(/&/g, "&amp;")
8
- .replace(/</g, "&lt;")
9
- .replace(/>/g, "&gt;")
10
- .replace(/"/g, "&quot;")
11
- .replace(/'/g, "&#039;");
7
+ .replace(/&/g, "&amp);")
8
+ .replace(/</g, "&lt);")
9
+ .replace(/>/g, "&gt);")
10
+ .replace(/"/g, "&quot);")
11
+ .replace(/'/g, "&#039);");
12
12
  }
13
13
 
14
14
  export function renderMusicSearchListHtml(search: MusicSearchResult): string {
@@ -25,16 +25,12 @@ function getBotAndTarget(ctx: any, event: any): {
25
25
  groupId?: number;
26
26
  userId?: number;
27
27
  } {
28
- const selfId = event?.self_id != null ? Number(event.self_id) : undefined;
29
- const bot =
30
- selfId != null && typeof ctx?.pickBot === "function"
31
- ? ctx.pickBot(selfId)
32
- : undefined;
28
+ const bot = event?.bot;
33
29
 
34
30
  return {
35
31
  bot,
36
- groupId: event?.message_type === "group" ? Number(event.group_id) : undefined,
37
- userId: event?.message_type !== "group" ? Number(event?.user_id) : undefined,
32
+ groupId: event?.message_type === "group" ? event.group_id : undefined,
33
+ userId: event?.message_type !== "group" ? event?.user_id : undefined,
38
34
  };
39
35
  }
40
36
 
@@ -46,7 +42,7 @@ export async function sendTextMessage(
46
42
  const { bot, groupId, userId } = getBotAndTarget(ctx, event);
47
43
  const payload: any[] = [];
48
44
 
49
- payload.push(ctx?.segment?.text ? ctx.segment.text(text) : { type: "text", text });
45
+ payload.push(ctx.segment.text(text));
50
46
 
51
47
  if (bot && groupId != null) {
52
48
  await bot.sendGroupMsg(groupId, payload);
@@ -71,11 +67,7 @@ export async function sendImageMessage(
71
67
  const { bot, groupId, userId } = getBotAndTarget(ctx, event);
72
68
  const sendPayload = async (source: string) => {
73
69
  const payload: any[] = [];
74
- payload.push(
75
- ctx?.segment?.image
76
- ? ctx.segment.image(normalizeFileSource(source))
77
- : { type: "image", file: normalizeFileSource(source) },
78
- );
70
+ payload.push(ctx.segment.image(normalizeFileSource(source)));
79
71
 
80
72
  if (bot && groupId != null) {
81
73
  await bot.sendGroupMsg(groupId, payload);
@@ -112,11 +104,7 @@ export async function sendFileMessage(
112
104
  const { bot, groupId, userId } = getBotAndTarget(ctx, event);
113
105
  const sendPayload = async (source: string) => {
114
106
  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
- );
107
+ payload.push(ctx.segment.file(normalizeFileSource(source), { name: fileName }));
120
108
 
121
109
  if (bot && groupId != null) {
122
110
  await bot.sendGroupMsg(groupId, payload);
@@ -184,11 +172,7 @@ export async function sendRecordMessage(
184
172
  const { bot, groupId, userId } = getBotAndTarget(ctx, event);
185
173
  const sendPayload = async (source: string) => {
186
174
  const payload: any[] = [];
187
- payload.push(
188
- ctx?.segment?.record
189
- ? ctx.segment.record(normalizeFileSource(source))
190
- : { type: "record", file: normalizeFileSource(source) },
191
- );
175
+ payload.push(ctx.segment.record(normalizeFileSource(source)));
192
176
 
193
177
  if (bot && groupId != null) {
194
178
  await bot.sendGroupMsg(groupId, payload);
@@ -414,19 +414,13 @@ export class MusicPluginRuntime {
414
414
  return;
415
415
  }
416
416
 
417
- const selfId = Number(event?.self_id || ctx?.self_id);
418
- if (!Number.isFinite(selfId) || selfId <= 0) {
419
- return;
420
- }
421
-
422
- const bot =
423
- typeof ctx?.pickBot === "function" ? ctx.pickBot(selfId) : undefined;
424
- if (!bot || typeof bot.api !== "function") {
425
- return;
417
+ const bot = event?.bot;
418
+ if (!bot || bot.adapter !== "onebotv11") {
419
+ return; // set_msg_emoji_like 是 onebot 专属特殊 API,icqq 不支持
426
420
  }
427
421
 
428
422
  try {
429
- await bot.api("set_msg_emoji_like", {
423
+ await bot.sendApi("set_msg_emoji_like", {
430
424
  message_id: messageId,
431
425
  emoji_id: MusicPluginRuntime.COMMAND_REACTION_FACE_ID,
432
426
  set: true,
@@ -0,0 +1,129 @@
1
+ import type { AISkill, AITool } from "mioku";
2
+ import type { MusicPluginRuntime } from "../runtime-core/service";
3
+
4
+ export function createMusicSkills(runtime: MusicPluginRuntime): AISkill[] {
5
+ return [
6
+ {
7
+ name: "music",
8
+ description:
9
+ "搜索音乐/歌曲/专辑/歌手,并可发送歌曲,注意,当用户想让你唱歌时,通常使用该技能而不是发送语音",
10
+ permission: "member",
11
+ tools: [
12
+ {
13
+ name: "search_music",
14
+ description: "按关键词搜索歌曲",
15
+ parameters: {
16
+ type: "object",
17
+ properties: {
18
+ query: {
19
+ type: "string",
20
+ description: "搜索关键词,可是歌曲名、歌手或专辑名",
21
+ },
22
+ },
23
+ required: ["query"],
24
+ },
25
+ handler: async (args: any, runtimeCtx?: any) => {
26
+ const ctx = runtimeCtx?.ctx;
27
+ const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
28
+ if (!event) {
29
+ return "music 插件尚未初始化";
30
+ }
31
+ const query = String(args?.query || "").trim();
32
+ if (!query) {
33
+ return "query 不能为空";
34
+ }
35
+ try {
36
+ const result = await runtime.searchSongs(event, query);
37
+ if (!result.tracks.length) {
38
+ return `没有找到「${query}」的歌曲`;
39
+ }
40
+ const list = result.tracks
41
+ .slice(0, 10)
42
+ .map(
43
+ (item, index) =>
44
+ `${index + 1}. ${item.title} - ${item.artist} (${item.album}) [${item.id}]`,
45
+ )
46
+ .join("\n");
47
+ return `已找到 ${result.tracks.length} 首:\n${list}`;
48
+ } catch (error) {
49
+ if (ctx) {
50
+ await runtime.notifyFailure(
51
+ ctx,
52
+ event,
53
+ `AI 发起音乐搜索失败。错误:${String(error)}。请简短告知稍后再试。`,
54
+ `搜索失败:${String(error)}`,
55
+ );
56
+ }
57
+ return `搜索失败:${String(error)}`;
58
+ }
59
+ },
60
+ } as AITool,
61
+ {
62
+ name: "send_music_song",
63
+ description:
64
+ "发送歌曲(通过 song_id 或 query)。默认发语音;传 as_file=true 时直接发源文件",
65
+ parameters: {
66
+ type: "object",
67
+ properties: {
68
+ song_id: {
69
+ type: "string",
70
+ description: "歌曲 ID,优先使用",
71
+ },
72
+ query: {
73
+ type: "string",
74
+ description: "关键词,内部会搜索后发送第一首",
75
+ },
76
+ as_file: {
77
+ type: "boolean",
78
+ description: "是否直接发送源文件",
79
+ default: false,
80
+ },
81
+ },
82
+ required: [],
83
+ },
84
+ handler: async (args: any, runtimeCtx?: any) => {
85
+ const ctx = runtimeCtx?.ctx;
86
+ const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
87
+ if (!ctx || !event) {
88
+ return "当前上下文不支持发送歌曲";
89
+ }
90
+
91
+ const songId = String(args?.song_id || "").trim();
92
+ const query = String(args?.query || "").trim();
93
+ const asFile = args?.as_file === true;
94
+ if (!songId && !query) {
95
+ return "必须提供 song_id 或 query";
96
+ }
97
+
98
+ ctx.logger.info(
99
+ `[music] send_music_song called: songId=${songId}, query=${query}, asFile=${asFile}`,
100
+ );
101
+
102
+ try {
103
+ if (songId) {
104
+ await runtime.sendSongById(ctx, event, songId, undefined, {
105
+ forceFile: asFile,
106
+ });
107
+ return asFile ? "已发送歌曲源文件" : "已发送歌曲";
108
+ }
109
+
110
+ await runtime.sendSongByQuery(ctx, event, query, {
111
+ forceFile: asFile,
112
+ });
113
+ return asFile ? "已按关键词发送歌曲源文件" : "已按关键词发送歌曲";
114
+ } catch (error) {
115
+ ctx.logger.error(`[music] send_music_song failed: ${error}`);
116
+ await runtime.notifyFailure(
117
+ ctx,
118
+ event,
119
+ `AI 发送歌曲失败。错误:${String(error)}。请简短提示稍后重试。`,
120
+ `发送失败:${String(error)}`,
121
+ );
122
+ return `发送失败:${String(error)}`;
123
+ }
124
+ },
125
+ } as AITool,
126
+ ],
127
+ },
128
+ ];
129
+ }
package/runtime.ts DELETED
@@ -1,24 +0,0 @@
1
- import type { MusicPluginRuntime } from "./runtime-core/service";
2
- import {
3
- getPluginRuntimeState,
4
- resetPluginRuntimeState,
5
- setPluginRuntimeState,
6
- } from "mioku";
7
-
8
- export interface MusicRuntimeState {
9
- runtime?: MusicPluginRuntime;
10
- }
11
-
12
- const MUSIC_PLUGIN_NAME = "music";
13
-
14
- export function getMusicRuntimeState(): MusicRuntimeState {
15
- return getPluginRuntimeState(MUSIC_PLUGIN_NAME) as MusicRuntimeState;
16
- }
17
-
18
- export function setMusicRuntimeState(nextState: MusicRuntimeState): MusicRuntimeState {
19
- return setPluginRuntimeState(MUSIC_PLUGIN_NAME, nextState) as MusicRuntimeState;
20
- }
21
-
22
- export function resetMusicRuntimeState(): void {
23
- resetPluginRuntimeState(MUSIC_PLUGIN_NAME);
24
- }
package/skills.ts DELETED
@@ -1,132 +0,0 @@
1
- import type { AISkill, AITool } from "mioku";
2
- import { getMusicRuntimeState } from "./runtime";
3
-
4
- const musicSkills: AISkill[] = [
5
- {
6
- name: "music",
7
- description:
8
- "搜索音乐/歌曲/专辑/歌手,并可发送歌曲,注意,当用户想让你唱歌时,通常使用该技能而不是发送语音",
9
- permission: "member",
10
- tools: [
11
- {
12
- name: "search_music",
13
- description: "按关键词搜索歌曲",
14
- parameters: {
15
- type: "object",
16
- properties: {
17
- query: {
18
- type: "string",
19
- description: "搜索关键词,可是歌曲名、歌手或专辑名",
20
- },
21
- },
22
- required: ["query"],
23
- },
24
- handler: async (args: any, runtimeCtx?: any) => {
25
- const runtime = getMusicRuntimeState().runtime;
26
- const ctx = runtimeCtx?.ctx;
27
- const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
28
- if (!runtime || !event) {
29
- return "music 插件尚未初始化";
30
- }
31
- const query = String(args?.query || "").trim();
32
- if (!query) {
33
- return "query 不能为空";
34
- }
35
- try {
36
- const result = await runtime.searchSongs(event, query);
37
- if (!result.tracks.length) {
38
- return `没有找到「${query}」的歌曲`;
39
- }
40
- const list = result.tracks
41
- .slice(0, 10)
42
- .map(
43
- (item, index) =>
44
- `${index + 1}. ${item.title} - ${item.artist} (${item.album}) [${item.id}]`,
45
- )
46
- .join("\n");
47
- return `已找到 ${result.tracks.length} 首:\n${list}`;
48
- } catch (error) {
49
- if (ctx) {
50
- await runtime.notifyFailure(
51
- ctx,
52
- event,
53
- `AI 发起音乐搜索失败。错误:${String(error)}。请简短告知稍后再试。`,
54
- `搜索失败:${String(error)}`,
55
- );
56
- }
57
- return `搜索失败:${String(error)}`;
58
- }
59
- },
60
- } as AITool,
61
- {
62
- name: "send_music_song",
63
- description:
64
- "发送歌曲(通过 song_id 或 query)。默认发语音;传 as_file=true 时直接发源文件",
65
- parameters: {
66
- type: "object",
67
- properties: {
68
- song_id: {
69
- type: "string",
70
- description: "歌曲 ID,优先使用",
71
- },
72
- query: {
73
- type: "string",
74
- description: "关键词,内部会搜索后发送第一首",
75
- },
76
- as_file: {
77
- type: "boolean",
78
- description: "是否直接发送源文件",
79
- default: false,
80
- },
81
- },
82
- required: [],
83
- },
84
- handler: async (args: any, runtimeCtx?: any) => {
85
- const runtimeState = getMusicRuntimeState();
86
- const runtime = runtimeState.runtime;
87
- const ctx = runtimeCtx?.ctx;
88
- const event = runtimeCtx?.event || runtimeCtx?.rawEvent;
89
- if (!runtime || !ctx || !event) {
90
- return "当前上下文不支持发送歌曲";
91
- }
92
-
93
- const songId = String(args?.song_id || "").trim();
94
- const query = String(args?.query || "").trim();
95
- const asFile = args?.as_file === true;
96
- if (!songId && !query) {
97
- return "必须提供 song_id 或 query";
98
- }
99
-
100
- ctx.logger.info(
101
- `[music] send_music_song called: songId=${songId}, query=${query}, asFile=${asFile}`,
102
- );
103
-
104
- try {
105
- if (songId) {
106
- await runtime.sendSongById(ctx, event, songId, undefined, {
107
- forceFile: asFile,
108
- });
109
- return asFile ? "已发送歌曲源文件" : "已发送歌曲";
110
- }
111
-
112
- await runtime.sendSongByQuery(ctx, event, query, {
113
- forceFile: asFile,
114
- });
115
- return asFile ? "已按关键词发送歌曲源文件" : "已按关键词发送歌曲";
116
- } catch (error) {
117
- ctx.logger.error(`[music] send_music_song failed: ${error}`);
118
- await runtime.notifyFailure(
119
- ctx,
120
- event,
121
- `AI 发送歌曲失败。错误:${String(error)}。请简短提示稍后重试。`,
122
- `发送失败:${String(error)}`,
123
- );
124
- return `发送失败:${String(error)}`;
125
- }
126
- },
127
- } as AITool,
128
- ],
129
- },
130
- ];
131
-
132
- export default musicSkills;