koishi-plugin-music-to-voice 1.0.0 → 1.0.1

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/dist/index.js CHANGED
@@ -32,372 +32,361 @@ var index_exports = {};
32
32
  __export(index_exports, {
33
33
  Config: () => Config,
34
34
  apply: () => apply,
35
- name: () => name
35
+ inject: () => inject,
36
+ name: () => name,
37
+ usage: () => usage
36
38
  });
37
39
  module.exports = __toCommonJS(index_exports);
38
40
  var import_koishi = require("koishi");
39
- var import_axios = __toESM(require("axios"));
40
- var import_node_fs = __toESM(require("fs"));
41
- var import_node_path = __toESM(require("path"));
42
41
  var import_node_os = __toESM(require("os"));
42
+ var import_node_fs = __toESM(require("fs"));
43
43
  var import_node_crypto = __toESM(require("crypto"));
44
- var name = "music-voice-pro";
44
+ var import_node_path = __toESM(require("path"));
45
+ var import_node_url = require("url");
46
+ var name = "music-to-voice";
45
47
  var logger = new import_koishi.Logger(name);
46
- var Config = import_koishi.Schema.object({
47
- commandName: import_koishi.Schema.string().default("\u542C\u6B4C").description("\u6307\u4EE4\u540D\u79F0"),
48
- commandAlias: import_koishi.Schema.string().default("music").description("\u6307\u4EE4\u522B\u540D"),
49
- apiBase: import_koishi.Schema.string().default("https://music-api.gdstudio.xyz/api.php").description("\u97F3\u4E50 API \u5730\u5740\uFF08GD\u97F3\u4E50\u53F0 API\uFF09"),
50
- source: import_koishi.Schema.union([
51
- import_koishi.Schema.const("netease").description("\u7F51\u6613\u4E91"),
52
- import_koishi.Schema.const("tencent").description("QQ\u97F3\u4E50"),
53
- import_koishi.Schema.const("kugou").description("\u9177\u72D7"),
54
- import_koishi.Schema.const("kuwo").description("\u9177\u6211"),
55
- import_koishi.Schema.const("migu").description("\u54AA\u5495"),
56
- import_koishi.Schema.const("baidu").description("\u767E\u5EA6")
57
- ]).default("netease").description("\u97F3\u6E90\uFF08\u4E0B\u62C9\u9009\u62E9\uFF09"),
58
- searchListCount: import_koishi.Schema.number().min(5).max(50).default(20).description("\u641C\u7D22\u5217\u8868\u6570\u91CF"),
59
- waitForTimeout: import_koishi.Schema.number().min(10).max(180).default(45).description("\u7B49\u5F85\u8F93\u5165\u5E8F\u53F7\u8D85\u65F6\uFF08\u79D2\uFF09"),
60
- nextPageCommand: import_koishi.Schema.string().default("\u4E0B\u4E00\u9875").description("\u4E0B\u4E00\u9875\u6307\u4EE4"),
61
- prevPageCommand: import_koishi.Schema.string().default("\u4E0A\u4E00\u9875").description("\u4E0A\u4E00\u9875\u6307\u4EE4"),
62
- exitCommandList: import_koishi.Schema.array(String).default(["0", "\u4E0D\u542C\u4E86", "\u9000\u51FA"]).description("\u9000\u51FA\u6307\u4EE4\u5217\u8868"),
63
- menuExitCommandTip: import_koishi.Schema.boolean().default(false).description("\u662F\u5426\u5728\u6B4C\u5355\u672B\u5C3E\u63D0\u793A\u9000\u51FA\u6307\u4EE4"),
64
- imageMode: import_koishi.Schema.boolean().default(false).description("\u56FE\u7247\u6B4C\u5355\u6A21\u5F0F\uFF08\u53EF\u9009\uFF1A\u9700\u8981 puppeteer \u63D2\u4EF6\uFF0C\u5F53\u524D\u4EC5\u4FDD\u7559\u5F00\u5173\uFF09"),
65
- sendAs: import_koishi.Schema.union([
66
- import_koishi.Schema.const("record").description("\u8BED\u97F3 record"),
67
- import_koishi.Schema.const("audio").description("\u97F3\u9891 audio")
68
- ]).default("record").description("\u53D1\u9001\u7C7B\u578B"),
69
- forceTranscode: import_koishi.Schema.boolean().default(true).description("\u662F\u5426\u5F3A\u5236\u8F6C\u7801\u4E3A silk\uFF08\u9700\u8981 ffmpeg + silk \u63D2\u4EF6\uFF09"),
70
- tempDir: import_koishi.Schema.string().default(import_node_path.default.join(import_node_os.default.tmpdir(), "koishi-music-voice")).description("\u4E34\u65F6\u76EE\u5F55"),
71
- cacheMinutes: import_koishi.Schema.number().min(0).max(1440).default(120).description("\u7F13\u5B58\u65F6\u957F\uFF08\u5206\u949F\uFF0C0=\u4E0D\u7F13\u5B58\uFF09"),
72
- generationTip: import_koishi.Schema.string().default("\u751F\u6210\u8BED\u97F3\u4E2D...").description("\u7528\u6237\u9009\u6B4C\u540E\u63D0\u793A"),
73
- recallSearchMenuMessage: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\uFF1A\u6B4C\u5355\u6D88\u606F"),
74
- recallTipMessage: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\uFF1A\u751F\u6210\u63D0\u793A\u6D88\u606F"),
75
- recallUserSelectMessage: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\uFF1A\u7528\u6237\u8F93\u5165\u7684\u5E8F\u53F7\u6D88\u606F"),
76
- recallVoiceMessage: import_koishi.Schema.boolean().default(false).description("\u64A4\u56DE\uFF1A\u8BED\u97F3\u6D88\u606F"),
77
- loggerinfo: import_koishi.Schema.boolean().default(false).description("\u65E5\u5FD7\u8C03\u8BD5\u6A21\u5F0F")
78
- }).description("\u70B9\u6B4C\u8BED\u97F3\uFF08\u652F\u6301\u7FFB\u9875 + \u53EF\u9009 silk/ffmpeg\uFF09");
79
- function safeText(x) {
80
- return typeof x === "string" ? x : x == null ? "" : String(x);
81
- }
82
- function normalizeSearchList(data) {
83
- const arr = Array.isArray(data) ? data : Array.isArray(data?.result) ? data.result : Array.isArray(data?.data) ? data.data : Array.isArray(data?.songs) ? data.songs : [];
84
- return arr.map((it) => {
85
- const id = safeText(it?.id ?? it?.songid ?? it?.rid ?? it?.hash ?? it?.mid);
86
- const name2 = safeText(it?.name ?? it?.songname ?? it?.title);
87
- const artist = safeText(it?.artist) || safeText(it?.singer) || safeText(it?.author) || (Array.isArray(it?.artists) ? it.artists.map((a) => safeText(a?.name)).filter(Boolean).join("/") : "");
88
- const album = safeText(it?.album ?? it?.albummid ?? it?.albumname);
89
- return { id, name: name2, artist, album };
90
- }).filter((x) => x.id && x.name);
91
- }
92
- function normalizeUrl(data) {
93
- return safeText(data?.url) || safeText(data?.data?.url) || safeText(data?.result?.url) || safeText(data?.data) || "";
94
- }
95
- function md5(s) {
96
- return import_node_crypto.default.createHash("md5").update(s).digest("hex");
97
- }
98
- function ensureDir(p) {
99
- import_node_fs.default.mkdirSync(p, { recursive: true });
48
+ var inject = {
49
+ required: ["http", "i18n"],
50
+ optional: ["puppeteer", "downloads", "ffmpeg", "silk"]
51
+ };
52
+ var usage = `
53
+ ---
54
+
55
+ \u672C\u63D2\u4EF6\u63D0\u4F9B\u201C\u70B9\u6B4C \u2192 \u5217\u8868\u9009\u5E8F\u53F7 \u2192 \u53D1\u9001\u8BED\u97F3/\u97F3\u9891/\u6587\u4EF6\u201D\u7684\u97F3\u4E50\u805A\u5408\u80FD\u529B\u3002
56
+
57
+ \u6570\u636E\u6765\u6E90\uFF1A**GD\u97F3\u4E50\u53F0 API**\uFF08https://music-api.gdstudio.xyz/api.php\uFF09
58
+
59
+ ---
60
+
61
+ ## \u5F00\u542F\u63D2\u4EF6\u524D\uFF0C\u8BF7\u786E\u4FDD\u4EE5\u4E0B\u670D\u52A1\u5DF2\u7ECF\u542F\u7528\uFF08\u53EF\u9009\u5B89\u88C5\uFF09
62
+
63
+ \u6240\u9700\u670D\u52A1\uFF1A
64
+
65
+ - puppeteer \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF0C\u7528\u4E8E\u56FE\u7247\u6B4C\u5355\uFF09
66
+
67
+ \u6B64\u5916\u53EF\u80FD\u8FD8\u9700\u8981\u8FD9\u4E9B\u670D\u52A1\u624D\u80FD\u53D1\u9001\u8BED\u97F3\uFF08\u6700\u7A33\uFF1A\u4E0B\u8F7D + \u8F6C\u7801\uFF09\uFF1A
68
+
69
+ - downloads \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09
70
+ - ffmpeg \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF0C\u53EF\u80FD\u4F9D\u8D56 downloads\uFF09
71
+ - silk \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09
72
+
73
+ ---
74
+ `;
75
+ var Config = import_koishi.Schema.intersect([
76
+ import_koishi.Schema.object({
77
+ commandName: import_koishi.Schema.string().default("\u542C\u6B4C").description("\u6307\u4EE4\u540D\u79F0"),
78
+ commandAlias: import_koishi.Schema.string().default("music2").description("\u6307\u4EE4\u522B\u540D\uFF08\u907F\u514D\u547D\u4EE4\u51B2\u7A81\uFF09")
79
+ }).description("\u57FA\u7840\u8BBE\u7F6E"),
80
+ import_koishi.Schema.object({
81
+ apiBase: import_koishi.Schema.string().default("https://music-api.gdstudio.xyz/api.php").description("GD\u97F3\u4E50\u53F0 API \u5730\u5740"),
82
+ source: import_koishi.Schema.union([
83
+ import_koishi.Schema.const("netease").description("\u6E901"),
84
+ import_koishi.Schema.const("tencent").description("\u6E902"),
85
+ import_koishi.Schema.const("kugou").description("\u6E903"),
86
+ import_koishi.Schema.const("kuwo").description("\u6E904"),
87
+ import_koishi.Schema.const("migu").description("\u6E905")
88
+ ]).default("kuwo").description("\u97F3\u6E90\uFF08\u4E0B\u62C9\u9009\u62E9\uFF09"),
89
+ searchListCount: import_koishi.Schema.natural().min(5).max(50).default(20).description("\u6BCF\u9875\u663E\u793A\u6761\u6570")
90
+ }).description("API \u8BBE\u7F6E"),
91
+ import_koishi.Schema.object({
92
+ imageMode: import_koishi.Schema.boolean().default(false).description("\u56FE\u7247\u6B4C\u5355\u6A21\u5F0F\uFF08\u9700\u8981 puppeteer\uFF0C\u53EF\u9009\u5B89\u88C5\uFF09"),
93
+ textColor: import_koishi.Schema.string().role("color").default("rgba(255,255,255,1)").description("\u56FE\u7247\u6B4C\u5355\u6587\u5B57\u989C\u8272"),
94
+ backgroundColor: import_koishi.Schema.string().role("color").default("rgba(0,0,0,1)").description("\u56FE\u7247\u6B4C\u5355\u80CC\u666F\u989C\u8272")
95
+ }).description("\u6B4C\u5355\u6837\u5F0F"),
96
+ import_koishi.Schema.object({
97
+ nextPageCommand: import_koishi.Schema.string().default("\u4E0B\u4E00\u9875").description("\u4E0B\u4E00\u9875\u6307\u4EE4"),
98
+ prevPageCommand: import_koishi.Schema.string().default("\u4E0A\u4E00\u9875").description("\u4E0A\u4E00\u9875\u6307\u4EE4"),
99
+ exitCommandList: import_koishi.Schema.array(import_koishi.Schema.string()).default(["0", "\u9000\u51FA", "\u4E0D\u542C\u4E86"]).description("\u9000\u51FA\u6307\u4EE4\u5217\u8868"),
100
+ menuExitCommandTip: import_koishi.Schema.boolean().default(false).description("\u662F\u5426\u5728\u6B4C\u5355\u5C3E\u90E8\u63D0\u793A\u9000\u51FA\u6307\u4EE4"),
101
+ waitForTimeout: import_koishi.Schema.natural().min(5).max(180).default(45).description("\u7B49\u5F85\u8F93\u5165\u5E8F\u53F7\u8D85\u65F6\uFF08\u79D2\uFF09"),
102
+ generationTip: import_koishi.Schema.string().default("\u751F\u6210\u8BED\u97F3\u4E2D\u2026").description("\u751F\u6210\u63D0\u793A\u6587\u672C")
103
+ }).description("\u4EA4\u4E92\u8BBE\u7F6E"),
104
+ import_koishi.Schema.object({
105
+ sendAs: import_koishi.Schema.union([
106
+ import_koishi.Schema.const("record").description("\u8BED\u97F3 record"),
107
+ import_koishi.Schema.const("audio").description("\u97F3\u9891 audio"),
108
+ import_koishi.Schema.const("file").description("\u6587\u4EF6 file")
109
+ ]).default("record").description("\u53D1\u9001\u7C7B\u578B"),
110
+ forceTranscode: import_koishi.Schema.boolean().default(true).description("\u6700\u7A33\u6A21\u5F0F\uFF1A\u4E0B\u8F7D + ffmpeg + silk \u8F6C\u7801\uFF08\u53EF\u9009\u4F9D\u8D56\uFF0C\u7F3A\u4F9D\u8D56\u81EA\u52A8\u964D\u7EA7\u76F4\u94FE\uFF09")
111
+ }).description("\u53D1\u9001\u8BBE\u7F6E"),
112
+ import_koishi.Schema.object({
113
+ recallMenu: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\u6B4C\u5355\u6D88\u606F"),
114
+ recallGeneratingTip: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\u751F\u6210\u63D0\u793A\u6D88\u606F"),
115
+ recallSongMessage: import_koishi.Schema.boolean().default(false).description("\u64A4\u56DE\u6B4C\u66F2\u6D88\u606F\uFF08\u8BED\u97F3/\u97F3\u9891/\u6587\u4EF6\uFF09"),
116
+ recallTimeoutTip: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\u8D85\u65F6\u63D0\u793A"),
117
+ recallExitTip: import_koishi.Schema.boolean().default(false).description("\u64A4\u56DE\u9000\u51FA\u63D0\u793A"),
118
+ recallInvalidTip: import_koishi.Schema.boolean().default(false).description("\u64A4\u56DE\u5E8F\u53F7\u9519\u8BEF\u63D0\u793A"),
119
+ recallFetchFailTip: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\u83B7\u53D6\u5931\u8D25\u63D0\u793A")
120
+ }).description("\u64A4\u56DE\u8BBE\u7F6E"),
121
+ import_koishi.Schema.object({
122
+ enableRateLimit: import_koishi.Schema.boolean().default(false).description("\u662F\u5426\u5F00\u542F\u9891\u7387\u9650\u5236"),
123
+ rateLimitScope: import_koishi.Schema.union([
124
+ import_koishi.Schema.const("user").description("\u6309\u7528\u6237"),
125
+ import_koishi.Schema.const("channel").description("\u6309\u9891\u9053"),
126
+ import_koishi.Schema.const("platform").description("\u6309\u5E73\u53F0")
127
+ ]).default("user").description("\u9650\u5236\u8303\u56F4"),
128
+ rateLimitIntervalSec: import_koishi.Schema.natural().min(1).max(3600).default(60).description("\u95F4\u9694\u79D2\u6570")
129
+ }).description("\u9891\u7387\u9650\u5236")
130
+ ]);
131
+ function artistsToString(x) {
132
+ if (!x) return "";
133
+ if (Array.isArray(x)) return x.join(" / ");
134
+ return String(x);
100
135
  }
101
- async function tryRecall(session, messageId) {
136
+ async function safeDelete(session, messageId) {
102
137
  if (!messageId) return;
138
+ const channelId = session.channelId;
139
+ if (!channelId) return;
103
140
  try {
104
- await session.bot.deleteMessage(session.channelId, messageId);
141
+ await session.bot.deleteMessage(channelId, messageId);
105
142
  } catch {
106
143
  }
107
144
  }
108
- function hRecord(src) {
109
- return (0, import_koishi.h)("record", { src });
110
- }
111
- function hAudio(src) {
112
- return (0, import_koishi.h)("audio", { src });
113
- }
114
- function buildMenuText(config, keyword, list, page) {
115
- const lines = [];
116
- lines.push(`NetEase Music:`);
117
- lines.push(`\u5173\u952E\u8BCD\uFF1A${keyword}`);
118
- lines.push(`\u97F3\u6E90\uFF1A${config.source} \u7B2C ${page} \u9875`);
119
- lines.push("");
120
- for (let i = 0; i < list.length; i++) {
121
- const it = list[i];
122
- const meta = [it.artist, it.album].filter(Boolean).join(" - ");
123
- lines.push(`${i + 1}. ${it.name}${meta ? ` -- ${meta}` : ""}`);
124
- }
125
- lines.push("");
126
- lines.push(`\u8BF7\u5728 ${config.waitForTimeout} \u79D2\u5185\u8F93\u5165\u5E8F\u53F7\uFF081-${list.length}\uFF09`);
127
- lines.push(`\u7FFB\u9875\uFF1A${config.prevPageCommand} / ${config.nextPageCommand}`);
128
- if (config.menuExitCommandTip) {
129
- lines.push(`\u9000\u51FA\uFF1A${config.exitCommandList.join(" / ")}`);
130
- }
131
- lines.push("");
132
- lines.push("\u6570\u636E\u6765\u6E90\uFF1AGD\u97F3\u4E50\u53F0 API");
133
- return lines.join("\n");
134
- }
135
- async function apiSearch(config, keyword, page = 1) {
136
- const params = {
137
- types: "search",
138
- source: config.source,
139
- name: keyword,
140
- count: config.searchListCount,
141
- pages: page
142
- };
143
- const { data } = await import_axios.default.get(config.apiBase, { params, timeout: 15e3 });
144
- return normalizeSearchList(data);
145
+ function buildRateKey(session, scope) {
146
+ if (scope === "platform") return session.platform;
147
+ if (scope === "channel") return `${session.platform}:${session.channelId ?? ""}`;
148
+ return `${session.platform}:${session.userId ?? ""}`;
145
149
  }
146
- async function apiGetSongUrl(config, id) {
147
- const params = { types: "url", source: config.source, id };
148
- const { data } = await import_axios.default.get(config.apiBase, { params, timeout: 15e3 });
149
- return normalizeUrl(data);
150
- }
151
- async function downloadToFile(url, filePath) {
152
- const res = await import_axios.default.get(url, { responseType: "stream", timeout: 3e4 });
153
- await new Promise((resolve, reject) => {
154
- const ws = import_node_fs.default.createWriteStream(filePath);
155
- res.data.pipe(ws);
156
- ws.on("finish", () => resolve());
157
- ws.on("error", reject);
150
+ async function gdSearch(ctx, config, keyword, page) {
151
+ const data = await ctx.http.get(config.apiBase, {
152
+ timeout: 15e3,
153
+ params: { types: "search", source: config.source, name: keyword, count: config.searchListCount, pages: page },
154
+ headers: { "user-agent": "koishi-music-to-voice" }
158
155
  });
156
+ if (!Array.isArray(data)) return [];
157
+ return data.map((x) => ({
158
+ id: String(x.id ?? x.url_id ?? ""),
159
+ name: String(x.name ?? ""),
160
+ artist: artistsToString(x.artist),
161
+ album: String(x.album ?? ""),
162
+ source: String(x.source ?? config.source)
163
+ })).filter((x) => x.id && x.name);
159
164
  }
160
- async function sendVoiceByUrl(session, config, audioUrl) {
161
- const seg = config.sendAs === "record" ? hRecord(audioUrl) : hAudio(audioUrl);
162
- const ids = await session.send(seg);
163
- return Array.isArray(ids) ? ids[0] : ids;
164
- }
165
- async function sendVoiceByFile(session, config, absPath) {
166
- const url = `file://${absPath.replace(/\\/g, "/")}`;
167
- const seg = config.sendAs === "record" ? hRecord(url) : hAudio(url);
168
- const ids = await session.send(seg);
169
- return Array.isArray(ids) ? ids[0] : ids;
165
+ async function gdUrl(ctx, config, item) {
166
+ const data = await ctx.http.get(config.apiBase, {
167
+ timeout: 15e3,
168
+ params: { types: "url", source: item.source || config.source, id: item.id },
169
+ headers: { "user-agent": "koishi-music-to-voice" }
170
+ });
171
+ if (typeof data === "string") return data;
172
+ if (data?.url) return String(data.url);
173
+ if (data?.data?.url) return String(data.data.url);
174
+ return "";
170
175
  }
171
- async function buildSilkIfPossible(ctx, config, audioUrl, cacheKey) {
172
- const hasFfmpeg = !!ctx.ffmpeg;
173
- const hasSilk = !!ctx.silk;
174
- if (!hasFfmpeg || !hasSilk) return null;
175
- ensureDir(config.tempDir);
176
- const silkPath = import_node_path.default.join(config.tempDir, `${cacheKey}.silk`);
177
- if (config.cacheMinutes > 0 && import_node_fs.default.existsSync(silkPath)) {
178
- return silkPath;
179
- }
180
- const rawPath = import_node_path.default.join(config.tempDir, `${cacheKey}.src`);
181
- await downloadToFile(audioUrl, rawPath);
182
- const wavPath = import_node_path.default.join(config.tempDir, `${cacheKey}.wav`);
183
- const ffmpeg = ctx.ffmpeg;
176
+ async function downloadToTemp(ctx, src) {
184
177
  try {
185
- if (typeof ffmpeg.convert === "function") {
186
- await ffmpeg.convert(rawPath, wavPath, {
187
- format: "wav",
188
- audioChannels: 1,
189
- audioFrequency: 24e3
190
- });
191
- } else if (typeof ffmpeg.exec === "function") {
192
- await ffmpeg.exec(["-y", "-i", rawPath, "-ac", "1", "-ar", "24000", wavPath]);
193
- } else {
194
- throw new Error("ffmpeg service API not recognized");
195
- }
178
+ const file = await ctx.http.file(src);
179
+ const ext = ".mp3";
180
+ const fp = import_node_path.default.join(import_node_os.default.tmpdir(), `music_${import_node_crypto.default.randomBytes(8).toString("hex")}${ext}`);
181
+ import_node_fs.default.writeFileSync(fp, Buffer.from(file.data));
182
+ return fp;
196
183
  } catch (e) {
197
- throw new Error(`ffmpeg \u8F6C\u7801\u5931\u8D25\uFF1A${e?.message || String(e)}`);
184
+ return null;
198
185
  }
199
- const silk = ctx.silk;
186
+ }
187
+ async function tryTranscode(ctx, localPath) {
188
+ const anyCtx = ctx;
189
+ if (!anyCtx.ffmpeg || !anyCtx.silk) return localPath;
190
+ const wav = localPath.replace(/\.\w+$/, "") + ".wav";
191
+ const silk = localPath.replace(/\.\w+$/, "") + ".silk";
200
192
  try {
201
- if (typeof silk.encode === "function") {
202
- await silk.encode(wavPath, silkPath, { rate: 24e3 });
203
- } else if (typeof silk.encodeWav === "function") {
204
- await silk.encodeWav(wavPath, silkPath, { rate: 24e3 });
205
- } else {
206
- throw new Error("silk service API not recognized");
207
- }
208
- } catch (e) {
209
- throw new Error(`silk \u7F16\u7801\u5931\u8D25\uFF1A${e?.message || String(e)}`);
193
+ await anyCtx.ffmpeg.convert(localPath, wav);
194
+ await anyCtx.silk.encode(wav, silk);
195
+ return silk;
196
+ } catch {
197
+ return localPath;
210
198
  } finally {
211
199
  try {
212
- import_node_fs.default.unlinkSync(rawPath);
213
- } catch {
214
- }
215
- try {
216
- import_node_fs.default.unlinkSync(wavPath);
200
+ import_node_fs.default.existsSync(wav) && import_node_fs.default.unlinkSync(wav);
217
201
  } catch {
218
202
  }
219
203
  }
220
- return silkPath;
221
204
  }
222
- function logDepsHint(ctx) {
223
- const hasPuppeteer = !!ctx.puppeteer;
224
- const hasFfmpeg = !!ctx.ffmpeg;
225
- const hasSilk = !!ctx.silk;
226
- const hasDownloads = !!ctx.downloads;
227
- logger.info("\u5F00\u542F\u63D2\u4EF6\u524D\uFF0C\u8BF7\u786E\u4FDD\u4EE5\u4E0B\u670D\u52A1\u5DF2\u7ECF\u542F\u7528\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A");
228
- logger.info(`- puppeteer\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A${hasPuppeteer ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
229
- logger.info("\u6B64\u5916\u53EF\u80FD\u8FD8\u9700\u8981\u8FD9\u4E9B\u670D\u52A1\u624D\u80FD\u53D1\u9001\u8BED\u97F3\uFF1A");
230
- logger.info(`- ffmpeg\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF08\u6B64\u670D\u52A1\u53EF\u80FD\u989D\u5916\u4F9D\u8D56downloads\u670D\u52A1\uFF09\uFF1A${hasFfmpeg ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
231
- logger.info(`- silk\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A${hasSilk ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
232
- logger.info(`- downloads\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A${hasDownloads ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
233
- logger.info("Music API \u51FA\u5904\uFF1AGD\u97F3\u4E50\u53F0 API\uFF08https://music-api.gdstudio.xyz/api.php\uFF09");
205
+ async function renderListImage(ctx, html) {
206
+ const anyCtx = ctx;
207
+ if (!anyCtx.puppeteer) return null;
208
+ const page = await anyCtx.puppeteer.page();
209
+ await page.setContent(html);
210
+ const el = await page.$("#song-list");
211
+ if (!el) return null;
212
+ const buf = await el.screenshot({});
213
+ await page.close();
214
+ return buf;
234
215
  }
235
- function apply(ctx, config) {
236
- if (config.loggerinfo) logger.level = import_koishi.Logger.DEBUG;
237
- logDepsHint(ctx);
238
- const pending = /* @__PURE__ */ new Map();
239
- function getKey(session) {
240
- return String(session?.channelId || "");
241
- }
242
- function isExit(input) {
243
- const t = input.trim();
244
- return config.exitCommandList.map((s) => s.trim()).includes(t);
245
- }
246
- function isExpired(state) {
247
- return Date.now() > state.expiresAt;
248
- }
249
- async function refreshMenu(session, state) {
250
- const list = await apiSearch(config, state.keyword, state.page);
251
- state.list = list;
252
- state.expiresAt = Date.now() + config.waitForTimeout * 1e3;
253
- if (config.recallSearchMenuMessage) {
254
- await tryRecall(session, state.menuMessageId);
255
- state.menuMessageId = void 0;
256
- }
257
- if (!config.recallSearchMenuMessage) {
258
- const text = buildMenuText(config, state.keyword, list, state.page);
259
- const ids = await session.send(text);
260
- state.menuMessageId = Array.isArray(ids) ? ids[0] : ids;
261
- } else {
262
- await session.send(`\u5DF2\u7FFB\u5230\u7B2C ${state.page} \u9875\uFF0C\u8BF7\u76F4\u63A5\u53D1\u9001\u5E8F\u53F7\uFF081-${list.length}\uFF09`);
263
- }
216
+ function makeListText(config, list, page) {
217
+ const start = (page - 1) * config.searchListCount;
218
+ const lines = list.map((s, i) => `${start + i + 1}. ${s.name} -- ${s.artist}${s.album ? " -- " + s.album : ""}`);
219
+ let tail = `
220
+
221
+ \u7FFB\u9875\uFF1A${config.prevPageCommand} / ${config.nextPageCommand}`;
222
+ if (config.menuExitCommandTip && config.exitCommandList?.length) {
223
+ tail += `
224
+ \u9000\u51FA\uFF1A${config.exitCommandList.join(" / ")}`;
264
225
  }
265
- async function handlePick(session, state, pickIndex) {
266
- const item = state.list[pickIndex];
267
- if (!item) {
268
- await session.send(`\u5E8F\u53F7\u65E0\u6548\uFF0C\u8BF7\u8F93\u5165 1-${state.list.length}\uFF0C\u6216\u8F93\u5165 ${config.exitCommandList.join("/")} \u9000\u51FA\u3002`);
269
- return;
270
- }
271
- let tipId;
272
- if (!config.recallTipMessage && config.generationTip?.trim()) {
273
- const ids = await session.send(config.generationTip);
274
- tipId = Array.isArray(ids) ? ids[0] : ids;
275
- }
276
- let songUrl = "";
277
- try {
278
- songUrl = await apiGetSongUrl(config, item.id);
279
- if (!songUrl) throw new Error("empty url");
280
- } catch {
281
- await session.send("\u83B7\u53D6\u6B4C\u66F2\u76F4\u94FE\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\uFF0C\u6216\u66F4\u6362\u97F3\u6E90\u3002");
282
- return;
226
+ return `\u97F3\u4E50\u5217\u8868\uFF1A
227
+ ${lines.join("\n")}${tail}
228
+
229
+ \u8BF7\u5728 ${config.waitForTimeout} \u79D2\u5185\u8F93\u5165\u5E8F\u53F7\uFF1A`;
230
+ }
231
+ function apply(ctx, config) {
232
+ const lastUse = /* @__PURE__ */ new Map();
233
+ ctx.command(`${config.commandName} <keyword:text>`, "\u97F3\u4E50\u805A\u5408\u70B9\u6B4C\u5E76\u53D1\u9001\u8BED\u97F3").alias(config.commandAlias).action(async (argv, keyword) => {
234
+ const session = argv.session;
235
+ const options = argv.options ?? {};
236
+ if (!session) return;
237
+ keyword = (keyword ?? "").trim();
238
+ if (!keyword) return `\u7528\u6CD5\uFF1A${config.commandName} \u6B4C\u66F2\u540D`;
239
+ if (config.enableRateLimit) {
240
+ const scope = config.rateLimitScope ?? "user";
241
+ const key = buildRateKey(session, scope);
242
+ const now = Date.now();
243
+ const last = lastUse.get(key) ?? 0;
244
+ const interval = (config.rateLimitIntervalSec ?? 60) * 1e3;
245
+ if (now - last < interval) {
246
+ const remain = Math.ceil((interval - (now - last)) / 1e3);
247
+ return `\u64CD\u4F5C\u592A\u9891\u7E41\uFF0C\u8BF7 ${remain} \u79D2\u540E\u518D\u8BD5\u3002`;
248
+ }
249
+ lastUse.set(key, now);
283
250
  }
284
- const cacheKey = md5(`${config.source}:${item.id}`);
285
- let voiceId;
286
- try {
287
- const silkPath = await buildSilkIfPossible(ctx, config, songUrl, cacheKey);
288
- if (silkPath) {
289
- voiceId = await sendVoiceByFile(session, config, silkPath);
290
- } else {
291
- if (config.forceTranscode) {
292
- await session.send(
293
- `\u5F53\u524D\u914D\u7F6E\u4E3A\u3010\u5F3A\u5236 silk \u8F6C\u7801\u3011\u4F46\u672A\u68C0\u6D4B\u5230 ffmpeg/silk \u670D\u52A1\u3002
294
- \u8BF7\u5728 Koishi \u63D2\u4EF6\u5E02\u573A\u5B89\u88C5\u5E76\u542F\u7528\uFF1Affmpeg\u3001silk\uFF08\u53EF\u80FD\u8FD8\u9700\u8981 downloads\uFF09\u3002`
295
- );
251
+ let page = 1;
252
+ let menuMsgId = null;
253
+ while (true) {
254
+ let list = [];
255
+ try {
256
+ list = await gdSearch(ctx, config, keyword, page);
257
+ } catch (e) {
258
+ logger.warn(`[search] ${String(e?.message || e)}`);
259
+ const ids = await session.send("\u641C\u7D22\u5931\u8D25\uFF08API \u4E0D\u53EF\u7528\u6216\u8D85\u65F6\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
260
+ const mid = Array.isArray(ids) ? ids[0] : ids;
261
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
262
+ return;
263
+ }
264
+ if (!list.length) {
265
+ const ids = await session.send("\u6CA1\u6709\u641C\u7D22\u5230\u7ED3\u679C\uFF0C\u8BF7\u6362\u4E2A\u5173\u952E\u8BCD\u8BD5\u8BD5\u3002");
266
+ const mid = Array.isArray(ids) ? ids[0] : ids;
267
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
268
+ return;
269
+ }
270
+ if (menuMsgId && config.recallMenu) await safeDelete(session, menuMsgId);
271
+ if (config.imageMode) {
272
+ const text2 = makeListText(config, list, page).replace(/\n/g, "<br/>");
273
+ const html = `<!doctype html><html><head><meta charset="utf-8"/><style>
274
+ body{margin:0;background:${config.backgroundColor};color:${config.textColor};font-size:16px;}
275
+ #song-list{padding:18px;white-space:nowrap;}
276
+ </style></head><body><div id="song-list">${text2}</div></body></html>`;
277
+ const buf = await renderListImage(ctx, html);
278
+ if (!buf) {
279
+ const ids2 = await session.send("\u56FE\u7247\u6B4C\u5355\u751F\u6210\u5931\u8D25\uFF1A\u672A\u5B89\u88C5 puppeteer \u670D\u52A1\u6216\u5176\u4E0D\u53EF\u7528\u3002");
280
+ const mid = Array.isArray(ids2) ? ids2[0] : ids2;
281
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
296
282
  return;
297
283
  }
298
- voiceId = await sendVoiceByUrl(session, config, songUrl);
284
+ const ids = await session.send([import_koishi.h.image(buf, "image/png")]);
285
+ menuMsgId = Array.isArray(ids) ? ids[0] : ids;
286
+ } else {
287
+ const txt = makeListText(config, list, page);
288
+ const ids = await session.send(txt);
289
+ menuMsgId = Array.isArray(ids) ? ids[0] : ids;
299
290
  }
300
- } catch (e) {
301
- await session.send(`\u751F\u6210\u8BED\u97F3\u5931\u8D25\uFF1A${e?.message || String(e)}
302
- \u8BF7\u68C0\u67E5 ffmpeg/silk \u63D2\u4EF6\u662F\u5426\u542F\u7528\uFF0C\u6216\u5173\u95ED\u201C\u5F3A\u5236\u8F6C\u7801\u201D\u3002`);
303
- return;
304
- } finally {
305
- state.tipMessageId = tipId;
306
- state.voiceMessageId = voiceId;
307
- }
308
- if (config.recallSearchMenuMessage) await tryRecall(session, state.menuMessageId);
309
- if (config.recallTipMessage) await tryRecall(session, state.tipMessageId);
310
- if (config.recallVoiceMessage) await tryRecall(session, state.voiceMessageId);
311
- pending.delete(getKey(session));
312
- }
313
- ctx.command(`${config.commandName} <keyword:text>`, "\u70B9\u6B4C\u5E76\u53D1\u9001\u8BED\u97F3\uFF08GD\u97F3\u4E50\u53F0 API\uFF09").alias(config.commandAlias).action(async ({ session }, keyword) => {
314
- if (!session) return;
315
- keyword = (keyword || "").trim();
316
- if (!keyword) return `\u7528\u6CD5\uFF1A${config.commandName} \u6B4C\u66F2\u540D`;
317
- const userId = session.userId;
318
- const channelId = session.channelId;
319
- if (!userId || !channelId) {
320
- await session.send("\u5F53\u524D\u9002\u914D\u5668\u672A\u63D0\u4F9B userId/channelId\uFF0C\u65E0\u6CD5\u8FDB\u5165\u9009\u6B4C\u6A21\u5F0F\u3002");
321
- return;
322
- }
323
- let list = [];
324
- try {
325
- list = await apiSearch(config, keyword, 1);
326
- } catch {
327
- return "\u641C\u7D22\u5931\u8D25\uFF08API \u4E0D\u53EF\u7528\u6216\u8D85\u65F6\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002";
328
- }
329
- if (!list.length) return "\u6CA1\u6709\u641C\u5230\u7ED3\u679C\uFF0C\u6362\u4E2A\u5173\u952E\u8BCD\u8BD5\u8BD5\u3002";
330
- const state = {
331
- userId,
332
- channelId,
333
- guildId: session.guildId,
334
- keyword,
335
- page: 1,
336
- list,
337
- expiresAt: Date.now() + config.waitForTimeout * 1e3
338
- };
339
- if (!config.recallSearchMenuMessage) {
340
- const text = buildMenuText(config, keyword, list, 1);
341
- const ids = await session.send(text);
342
- state.menuMessageId = Array.isArray(ids) ? ids[0] : ids;
343
- } else {
344
- await session.send(`\u5DF2\u8FDB\u5165\u9009\u6B4C\u6A21\u5F0F\uFF0C\u8BF7\u76F4\u63A5\u53D1\u9001\u5E8F\u53F7\uFF081-${list.length}\uFF09\uFF0C\u6216\u53D1\u9001\u201C${config.nextPageCommand}/${config.prevPageCommand}\u201D\u7FFB\u9875\u3002`);
345
- }
346
- pending.set(channelId, state);
347
- });
348
- ctx.middleware(async (session, next) => {
349
- const key = getKey(session);
350
- if (!key) return next();
351
- const state = pending.get(key);
352
- if (!state) return next();
353
- if (session.userId !== state.userId) return next();
354
- if (isExpired(state)) {
355
- pending.delete(key);
356
- return next();
357
- }
358
- const content = (session.content || "").trim();
359
- if (!content) return next();
360
- if (isExit(content)) {
361
- if (config.recallSearchMenuMessage) await tryRecall(session, state.menuMessageId);
362
- pending.delete(key);
363
- if (!config.recallUserSelectMessage) await session.send("\u5DF2\u9000\u51FA\u9009\u6B4C\u3002");
364
- return;
365
- }
366
- if (content === config.nextPageCommand) {
367
- if (config.recallUserSelectMessage) await tryRecall(session, session.messageId);
368
- state.page += 1;
291
+ const input = await session.prompt(
292
+ (s) => {
293
+ const els = s.elements ?? [];
294
+ return import_koishi.h.select(els, "text").join("");
295
+ },
296
+ { timeout: config.waitForTimeout * 1e3 }
297
+ );
298
+ if ((0, import_koishi.isNullable)(input)) {
299
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
300
+ const ids = await session.send("\u8F93\u5165\u8D85\u65F6\uFF0C\u5DF2\u53D6\u6D88\u70B9\u6B4C\u3002");
301
+ const mid = Array.isArray(ids) ? ids[0] : ids;
302
+ if (config.recallTimeoutTip) await safeDelete(session, mid);
303
+ return;
304
+ }
305
+ const text = String(input).trim();
306
+ if (!text) continue;
307
+ if (config.exitCommandList.includes(text)) {
308
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
309
+ const ids = await session.send("\u5DF2\u9000\u51FA\u6B4C\u66F2\u9009\u62E9\u3002");
310
+ const mid = Array.isArray(ids) ? ids[0] : ids;
311
+ if (config.recallExitTip) await safeDelete(session, mid);
312
+ return;
313
+ }
314
+ if (text === config.nextPageCommand) {
315
+ page += 1;
316
+ continue;
317
+ }
318
+ if (text === config.prevPageCommand) {
319
+ page = Math.max(1, page - 1);
320
+ continue;
321
+ }
322
+ if (!/^\d+$/.test(text)) continue;
323
+ const idx = parseInt(text, 10);
324
+ const start = (page - 1) * config.searchListCount;
325
+ const local = idx - start - 1;
326
+ if (local < 0 || local >= list.length) {
327
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
328
+ const ids = await session.send("\u5E8F\u53F7\u8F93\u5165\u9519\u8BEF\uFF0C\u5DF2\u9000\u51FA\u6B4C\u66F2\u9009\u62E9\u3002");
329
+ const mid = Array.isArray(ids) ? ids[0] : ids;
330
+ if (config.recallInvalidTip) await safeDelete(session, mid);
331
+ return;
332
+ }
333
+ const chosen = list[local];
334
+ let tipMsgId = null;
335
+ {
336
+ const ids = await session.send(config.generationTip);
337
+ tipMsgId = Array.isArray(ids) ? ids[0] : ids;
338
+ }
339
+ let playUrl = "";
369
340
  try {
370
- await refreshMenu(session, state);
371
- } catch {
372
- state.page -= 1;
373
- await session.send("\u7FFB\u9875\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
341
+ playUrl = await gdUrl(ctx, config, chosen);
342
+ } catch (e) {
343
+ logger.warn(`[url] ${String(e?.message || e)}`);
374
344
  }
375
- return;
376
- }
377
- if (content === config.prevPageCommand) {
378
- if (config.recallUserSelectMessage) await tryRecall(session, session.messageId);
379
- if (state.page <= 1) {
380
- await session.send("\u5DF2\u7ECF\u662F\u7B2C\u4E00\u9875\u3002");
345
+ if (!playUrl) {
346
+ if (config.recallGeneratingTip) await safeDelete(session, tipMsgId);
347
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
348
+ const ids = await session.send("\u83B7\u53D6\u6B4C\u66F2\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
349
+ const mid = Array.isArray(ids) ? ids[0] : ids;
350
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
381
351
  return;
382
352
  }
383
- state.page -= 1;
384
353
  try {
385
- await refreshMenu(session, state);
386
- } catch {
387
- state.page += 1;
388
- await session.send("\u7FFB\u9875\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
354
+ let finalSrc = playUrl;
355
+ if (config.forceTranscode) {
356
+ const localPath = await downloadToTemp(ctx, playUrl);
357
+ if (localPath) {
358
+ const out = await tryTranscode(ctx, localPath);
359
+ finalSrc = (0, import_node_url.pathToFileURL)(out).href;
360
+ }
361
+ }
362
+ let sendIds;
363
+ if (config.sendAs === "file") {
364
+ sendIds = await session.send([import_koishi.h.file(finalSrc)]);
365
+ } else if (config.sendAs === "audio") {
366
+ sendIds = await session.send([import_koishi.h.audio(finalSrc)]);
367
+ } else {
368
+ sendIds = await session.send([(0, import_koishi.h)("record", { src: finalSrc })]);
369
+ }
370
+ const songMsgId = Array.isArray(sendIds) ? sendIds[0] : sendIds;
371
+ if (config.recallSongMessage) await safeDelete(session, songMsgId);
372
+ } catch (e) {
373
+ logger.warn(`[send] ${String(e?.message || e)}`);
374
+ const ids = await session.send("\u53D1\u9001\u5931\u8D25\uFF08\u53EF\u80FD\u7F3A\u5C11\u8F6C\u7801\u4F9D\u8D56\u6216\u94FE\u63A5\u5931\u6548\uFF09\u3002");
375
+ const mid = Array.isArray(ids) ? ids[0] : ids;
376
+ if (config.recallFetchFailTip) await safeDelete(session, mid);
377
+ } finally {
378
+ if (config.recallGeneratingTip) await safeDelete(session, tipMsgId);
379
+ if (config.recallMenu) await safeDelete(session, menuMsgId);
389
380
  }
390
381
  return;
391
382
  }
392
- const n = Number(content);
393
- if (!Number.isInteger(n) || n < 1 || n > state.list.length) return next();
394
- if (config.recallUserSelectMessage) await tryRecall(session, session.messageId);
395
- await handlePick(session, state, n - 1);
396
383
  });
397
384
  }
398
385
  // Annotate the CommonJS export names for ESM import in node:
399
386
  0 && (module.exports = {
400
387
  Config,
401
388
  apply,
402
- name
389
+ inject,
390
+ name,
391
+ usage
403
392
  });