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

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,323 @@ 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
41
  var import_node_fs = __toESM(require("fs"));
41
42
  var import_node_path = __toESM(require("path"));
42
43
  var import_node_os = __toESM(require("os"));
43
44
  var import_node_crypto = __toESM(require("crypto"));
44
- var name = "music-voice-pro";
45
+ var import_node_child_process = require("child_process");
46
+ var name = "music-to-voice";
45
47
  var logger = new import_koishi.Logger(name);
48
+ var inject = {
49
+ optional: ["downloads", "ffmpeg", "silk", "puppeteer"]
50
+ };
51
+ var usage = `
52
+ ### \u70B9\u6B4C\u8BED\u97F3\uFF08\u652F\u6301\u7FFB\u9875 + \u53EF\u9009 silk/ffmpeg\uFF09
53
+
54
+ \u5F00\u542F\u63D2\u4EF6\u524D\uFF0C\u8BF7\u786E\u4FDD\u4EE5\u4E0B\u670D\u52A1\u5DF2\u7ECF\u542F\u7528\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A
55
+
56
+ - **puppeteer \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09**
57
+
58
+ \u6B64\u5916\u53EF\u80FD\u8FD8\u9700\u8981\u8FD9\u4E9B\u670D\u52A1\u624D\u80FD\u53D1\u9001\u8BED\u97F3\uFF1A
59
+
60
+ - **ffmpeg \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09**\uFF08\u6B64\u670D\u52A1\u53EF\u80FD\u989D\u5916\u4F9D\u8D56 **downloads** \u670D\u52A1\uFF09
61
+ - **silk \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09**
62
+
63
+ > \u672C\u63D2\u4EF6\u4F7F\u7528\u97F3\u4E50\u805A\u5408\u63A5\u53E3\uFF08GD\u97F3\u4E50\u53F0 API\uFF09\uFF1Ahttps://music-api.gdstudio.xyz/api.php
64
+ `;
46
65
  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"),
66
+ commandName: import_koishi.Schema.string().description("\u6307\u4EE4\u540D\u79F0").default("\u542C\u6B4C"),
67
+ commandAlias: import_koishi.Schema.string().description("\u6307\u4EE4\u522B\u540D").default("music"),
68
+ apiBase: import_koishi.Schema.string().description("\u97F3\u4E50 API \u5730\u5740\uFF08GD\u97F3\u4E50\u53F0 API\uFF09").default("https://music-api.gdstudio.xyz/api.php"),
69
+ // ✅ 后台显示品牌名(你要求的)
50
70
  source: import_koishi.Schema.union([
51
71
  import_koishi.Schema.const("netease").description("\u7F51\u6613\u4E91"),
52
72
  import_koishi.Schema.const("tencent").description("QQ\u97F3\u4E50"),
53
73
  import_koishi.Schema.const("kugou").description("\u9177\u72D7"),
54
74
  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"),
75
+ import_koishi.Schema.const("migu").description("\u54AA\u5495")
76
+ ]).description("\u97F3\u6E90\uFF08\u4E0B\u62C9\u9009\u62E9\uFF09").default("kuwo"),
77
+ searchListCount: import_koishi.Schema.natural().min(1).max(30).step(1).description("\u641C\u7D22\u5217\u8868\u6570\u91CF").default(20),
78
+ waitForTimeout: import_koishi.Schema.natural().min(5).max(300).step(1).description("\u7B49\u5F85\u8F93\u5165\u5E8F\u53F7\u8D85\u65F6\uFF08\u79D2\uFF09").default(45),
79
+ nextPageCommand: import_koishi.Schema.string().description("\u4E0B\u4E00\u9875\u6307\u4EE4").default("\u4E0B\u4E00\u9875"),
80
+ prevPageCommand: import_koishi.Schema.string().description("\u4E0A\u4E00\u9875\u6307\u4EE4").default("\u4E0A\u4E00\u9875"),
81
+ exitCommandList: import_koishi.Schema.array(import_koishi.Schema.string()).role("table").description("\u9000\u51FA\u6307\u4EE4\u5217\u8868\uFF08\u4E00\u884C\u4E00\u4E2A\uFF09").default(["0", "\u4E0D\u542C\u4E86", "\u9000\u51FA"]),
82
+ menuExitCommandTip: import_koishi.Schema.boolean().description("\u662F\u5426\u5728\u6B4C\u5355\u672B\u5C3E\u63D0\u793A\u9000\u51FA\u6307\u4EE4").default(false),
83
+ // ✅ 解决“太快撤回”的关键:默认 60 秒撤回歌单;并且默认“发送成功才撤回”
84
+ menuRecallSec: import_koishi.Schema.natural().min(0).max(3600).step(1).description("\u6B4C\u5355\u64A4\u56DE\u79D2\u6570\uFF080=\u4E0D\u64A4\u56DE\uFF09").default(60),
85
+ tipRecallSec: import_koishi.Schema.natural().min(0).max(3600).step(1).description("\u201C\u751F\u6210\u4E2D\u201D\u63D0\u793A\u64A4\u56DE\u79D2\u6570\uFF080=\u4E0D\u64A4\u56DE\uFF09").default(10),
86
+ recallOnlyAfterSuccess: import_koishi.Schema.boolean().description("\u4EC5\u5728\u53D1\u9001\u6210\u529F\u540E\u624D\u64A4\u56DE\uFF08\u63A8\u8350\u5F00\u542F\uFF09").default(true),
87
+ keepMenuIfSendFailed: import_koishi.Schema.boolean().description("\u53D1\u9001\u5931\u8D25\u65F6\u4FDD\u7559\u6B4C\u5355\uFF08\u63A8\u8350\u5F00\u542F\uFF09").default(true),
65
88
  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 });
100
- }
101
- async function tryRecall(session, messageId) {
102
- if (!messageId) return;
103
- try {
104
- await session.bot.deleteMessage(session.channelId, messageId);
105
- } catch {
106
- }
89
+ import_koishi.Schema.const("record").description("\u8BED\u97F3 record\uFF08\u63A8\u8350\uFF09"),
90
+ import_koishi.Schema.const("audio").description("\u97F3\u9891 audio"),
91
+ import_koishi.Schema.const("file").description("\u6587\u4EF6 file")
92
+ ]).description("\u53D1\u9001\u7C7B\u578B").default("record"),
93
+ // 装了 downloads+ffmpeg+silk 后会更稳(QQ 语音经常只认 silk)
94
+ forceTranscode: import_koishi.Schema.boolean().description("\u5F3A\u5236\u8F6C\u7801\uFF08\u9700\u8981 downloads + ffmpeg + silk\uFF1B\u66F4\u7A33\u4F46\u4F9D\u8D56\u66F4\u591A\uFF09").default(true),
95
+ maxSongDuration: import_koishi.Schema.natural().min(0).max(180).step(1).description("\u6B4C\u66F2\u6700\u957F\u65F6\u957F\uFF08\u5206\u949F\uFF0C0=\u4E0D\u9650\u5236\uFF09").default(30),
96
+ userAgent: import_koishi.Schema.string().description("\u8BF7\u6C42 UA\uFF08\u90E8\u5206\u73AF\u5883\u53EF\u907F\u514D\u98CE\u63A7/403\uFF09").default("koishi-music-to-voice/1.0"),
97
+ generationTip: import_koishi.Schema.string().description("\u9009\u62E9\u5E8F\u53F7\u540E\u53D1\u9001\u7684\u63D0\u793A\u6587\u6848").default("\u97F3\u4E50\u751F\u6210\u4E2D\u2026")
98
+ });
99
+ var pending = /* @__PURE__ */ new Map();
100
+ function ms(sec) {
101
+ return Math.max(1, sec) * 1e3;
107
102
  }
108
- function hRecord(src) {
109
- return (0, import_koishi.h)("record", { src });
103
+ function keyOf(session) {
104
+ return `${session.platform}:${session.userId || "unknown"}:${session.channelId || session.guildId || "unknown"}`;
110
105
  }
111
- function hAudio(src) {
112
- return (0, import_koishi.h)("audio", { src });
106
+ function normalizeArtist(a) {
107
+ if (!a) return "";
108
+ if (Array.isArray(a)) return a.join(" / ");
109
+ return String(a);
113
110
  }
114
- function buildMenuText(config, keyword, list, page) {
111
+ function formatMenu(state, config) {
115
112
  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`);
113
+ lines.push(`\u70B9\u6B4C\u5217\u8868\uFF08\u7B2C ${state.page} \u9875\uFF09`);
114
+ lines.push(`\u5173\u952E\u8BCD\uFF1A${state.keyword}`);
119
115
  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
- }
116
+ state.items.forEach((it, idx) => {
117
+ const n = idx + 1;
118
+ const artist = normalizeArtist(it.artist);
119
+ lines.push(`${n}. ${it.name}${artist ? ` - ${artist}` : ""}`);
120
+ });
125
121
  lines.push("");
126
- lines.push(`\u8BF7\u5728 ${config.waitForTimeout} \u79D2\u5185\u8F93\u5165\u5E8F\u53F7\uFF081-${list.length}\uFF09`);
122
+ lines.push(`\u8BF7\u5728 ${config.waitForTimeout} \u79D2\u5185\u8F93\u5165\u6B4C\u66F2\u5E8F\u53F7`);
127
123
  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");
124
+ if (config.menuExitCommandTip) lines.push(`\u9000\u51FA\uFF1A${config.exitCommandList.join(" / ")}`);
133
125
  return lines.join("\n");
134
126
  }
135
- async function apiSearch(config, keyword, page = 1) {
136
- const params = {
127
+ async function safeSend(session, content) {
128
+ const ids = await session.send(content);
129
+ if (Array.isArray(ids)) return ids.filter(Boolean);
130
+ return ids ? [ids] : [];
131
+ }
132
+ function recall(session, ids, sec) {
133
+ if (!ids?.length || sec <= 0) return;
134
+ const channelId = session.channelId;
135
+ if (!channelId) return;
136
+ setTimeout(() => {
137
+ ids.forEach((id) => session.bot.deleteMessage(channelId, id).catch(() => {
138
+ }));
139
+ }, sec * 1e3);
140
+ }
141
+ async function apiSearch(ctx, config, keyword, page) {
142
+ const params = new URLSearchParams({
137
143
  types: "search",
138
144
  source: config.source,
139
145
  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
- }
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);
146
+ count: String(config.searchListCount),
147
+ pages: String(page)
148
+ });
149
+ const url = `${config.apiBase}?${params.toString()}`;
150
+ const data = await ctx.http.get(url, {
151
+ headers: { "user-agent": config.userAgent },
152
+ responseType: "json",
153
+ timeout: 15e3
158
154
  });
155
+ if (!Array.isArray(data)) throw new Error("search response is not array");
156
+ return data;
159
157
  }
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;
158
+ async function apiGetSongUrl(ctx, config, item) {
159
+ const id = item.url_id || item.id;
160
+ const params = new URLSearchParams({
161
+ types: "url",
162
+ id: String(id),
163
+ source: item.source || config.source
164
+ });
165
+ const url = `${config.apiBase}?${params.toString()}`;
166
+ const data = await ctx.http.get(url, {
167
+ headers: { "user-agent": config.userAgent },
168
+ responseType: "json",
169
+ timeout: 15e3
170
+ });
171
+ const u = Array.isArray(data) ? data[0]?.url : data?.url;
172
+ if (!u) throw new Error("url not found from api");
173
+ return u;
164
174
  }
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;
175
+ function tmpFile(ext) {
176
+ const id = import_node_crypto.default.randomBytes(8).toString("hex");
177
+ return import_node_path.default.join(import_node_os.default.tmpdir(), `koishi-music-${id}.${ext}`);
170
178
  }
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;
184
- 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
- }
196
- } catch (e) {
197
- throw new Error(`ffmpeg \u8F6C\u7801\u5931\u8D25\uFF1A${e?.message || String(e)}`);
198
- }
199
- const silk = ctx.silk;
200
- 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)}`);
210
- } finally {
211
- try {
212
- import_node_fs.default.unlinkSync(rawPath);
213
- } catch {
214
- }
215
- try {
216
- import_node_fs.default.unlinkSync(wavPath);
217
- } catch {
218
- }
179
+ async function downloadToFile(ctx, config, url, filePath) {
180
+ const anyCtx = ctx;
181
+ if (anyCtx.downloads?.download) {
182
+ await anyCtx.downloads.download(url, filePath, {
183
+ headers: { "user-agent": config.userAgent }
184
+ });
185
+ return;
219
186
  }
220
- return silkPath;
187
+ const buf = await ctx.http.get(url, {
188
+ headers: { "user-agent": config.userAgent },
189
+ responseType: "arraybuffer",
190
+ timeout: 3e4
191
+ });
192
+ import_node_fs.default.writeFileSync(filePath, Buffer.from(buf));
221
193
  }
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");
194
+ function runFfmpegToPcm(input, output) {
195
+ return new Promise((resolve, reject) => {
196
+ const p = (0, import_node_child_process.spawn)("ffmpeg", ["-y", "-i", input, "-ac", "1", "-ar", "48000", "-f", "s16le", output], { stdio: "ignore" });
197
+ p.on("error", reject);
198
+ p.on("exit", (code) => code === 0 ? resolve() : reject(new Error(`ffmpeg exit code ${code}`)));
199
+ });
234
200
  }
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;
201
+ async function encodeSilk(ctx, pcmPath) {
202
+ const anyCtx = ctx;
203
+ if (anyCtx.silk?.encode) {
204
+ const pcm = import_node_fs.default.readFileSync(pcmPath);
205
+ const out = await anyCtx.silk.encode(pcm, 48e3);
206
+ return Buffer.isBuffer(out) ? out : Buffer.from(out);
248
207
  }
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
- }
264
- }
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;
283
- }
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
- );
296
- return;
208
+ throw new Error("silk service encode not available");
209
+ }
210
+ async function sendSong(session, ctx, config, url) {
211
+ if (config.sendAs === "record" && config.forceTranscode) {
212
+ const anyCtx = ctx;
213
+ if (anyCtx.downloads && anyCtx.silk) {
214
+ try {
215
+ const inFile = tmpFile("mp3");
216
+ const pcmFile = tmpFile("pcm");
217
+ await downloadToFile(ctx, config, url, inFile);
218
+ await runFfmpegToPcm(inFile, pcmFile);
219
+ const silkBuf = await encodeSilk(ctx, pcmFile);
220
+ try {
221
+ import_node_fs.default.unlinkSync(inFile);
222
+ } catch {
223
+ }
224
+ try {
225
+ import_node_fs.default.unlinkSync(pcmFile);
226
+ } catch {
297
227
  }
298
- voiceId = await sendVoiceByUrl(session, config, songUrl);
228
+ return await safeSend(session, (0, import_koishi.h)("record", { src: silkBuf }));
229
+ } catch (e) {
230
+ logger.warn("transcode/send record failed, fallback: %s", e.message);
299
231
  }
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
232
  }
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
233
  }
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) => {
234
+ if (config.sendAs === "record") return await safeSend(session, (0, import_koishi.h)("record", { src: url }));
235
+ if (config.sendAs === "audio") return await safeSend(session, import_koishi.h.audio(url));
236
+ return await safeSend(session, import_koishi.h.file(url));
237
+ }
238
+ function apply(ctx, config) {
239
+ ctx.command(`${config.commandName} <keyword:text>`, "\u70B9\u6B4C\u5E76\u53D1\u9001\u8BED\u97F3/\u97F3\u9891").alias(config.commandAlias).action(async ({ session }, keyword) => {
314
240
  if (!session) return;
315
241
  keyword = (keyword || "").trim();
316
242
  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 = [];
243
+ const k = keyOf(session);
244
+ const old = pending.get(k);
245
+ if (old?.timer) clearTimeout(old.timer);
246
+ pending.delete(k);
247
+ let items;
324
248
  try {
325
- list = await apiSearch(config, keyword, 1);
326
- } catch {
249
+ items = await apiSearch(ctx, config, keyword, 1);
250
+ } catch (e) {
251
+ logger.warn("search failed: %s", e.message);
327
252
  return "\u641C\u7D22\u5931\u8D25\uFF08API \u4E0D\u53EF\u7528\u6216\u8D85\u65F6\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002";
328
253
  }
329
- if (!list.length) return "\u6CA1\u6709\u641C\u5230\u7ED3\u679C\uFF0C\u6362\u4E2A\u5173\u952E\u8BCD\u8BD5\u8BD5\u3002";
254
+ if (!items.length) return "\u6CA1\u6709\u641C\u7D22\u5230\u7ED3\u679C\u3002";
330
255
  const state = {
331
- userId,
332
- channelId,
333
- guildId: session.guildId,
256
+ userId: session.userId || "",
257
+ channelId: session.channelId || "",
334
258
  keyword,
335
259
  page: 1,
336
- list,
337
- expiresAt: Date.now() + config.waitForTimeout * 1e3
260
+ items,
261
+ menuMessageIds: [],
262
+ tipMessageIds: []
338
263
  };
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`);
264
+ pending.set(k, state);
265
+ const menuText = formatMenu(state, config);
266
+ state.menuMessageIds = await safeSend(session, menuText);
267
+ if (config.menuRecallSec > 0 && !config.recallOnlyAfterSuccess) {
268
+ recall(session, state.menuMessageIds, config.menuRecallSec);
345
269
  }
346
- pending.set(channelId, state);
270
+ state.timer = setTimeout(async () => {
271
+ const cur = pending.get(k);
272
+ if (!cur) return;
273
+ pending.delete(k);
274
+ await session.send("\u8F93\u5165\u8D85\u65F6\uFF0C\u5DF2\u53D6\u6D88\u70B9\u6B4C\u3002");
275
+ }, ms(config.waitForTimeout));
276
+ return;
347
277
  });
348
278
  ctx.middleware(async (session, next) => {
349
- const key = getKey(session);
350
- if (!key) return next();
351
- const state = pending.get(key);
279
+ const k = keyOf(session);
280
+ const state = pending.get(k);
352
281
  if (!state) return next();
353
- if (session.userId !== state.userId) return next();
354
- if (isExpired(state)) {
355
- pending.delete(key);
356
- return next();
357
- }
282
+ if ((session.userId || "") !== state.userId || (session.channelId || "") !== state.channelId) return next();
358
283
  const content = (session.content || "").trim();
359
284
  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");
285
+ if (config.exitCommandList.map((s) => s.trim()).filter(Boolean).includes(content)) {
286
+ pending.delete(k);
287
+ if (state.timer) clearTimeout(state.timer);
288
+ await session.send("\u5DF2\u9000\u51FA\u6B4C\u66F2\u9009\u62E9\u3002");
364
289
  return;
365
290
  }
366
- if (content === config.nextPageCommand) {
367
- if (config.recallUserSelectMessage) await tryRecall(session, session.messageId);
368
- state.page += 1;
291
+ if (content === config.nextPageCommand || content === config.prevPageCommand) {
292
+ const target = content === config.nextPageCommand ? state.page + 1 : Math.max(1, state.page - 1);
293
+ if (target === state.page) {
294
+ await session.send("\u5DF2\u7ECF\u662F\u7B2C\u4E00\u9875\u3002");
295
+ return;
296
+ }
369
297
  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");
298
+ const items = await apiSearch(ctx, config, state.keyword, target);
299
+ if (!items.length) {
300
+ await session.send("\u6CA1\u6709\u66F4\u591A\u7ED3\u679C\u4E86\u3002");
301
+ return;
302
+ }
303
+ state.page = target;
304
+ state.items = items;
305
+ const menuText = formatMenu(state, config);
306
+ const newIds = await safeSend(session, menuText);
307
+ state.menuMessageIds.push(...newIds);
308
+ if (config.menuRecallSec > 0 && !config.recallOnlyAfterSuccess) recall(session, newIds, config.menuRecallSec);
309
+ } catch (e) {
310
+ logger.warn("page search failed: %s", e.message);
311
+ await session.send("\u7FFB\u9875\u5931\u8D25\uFF08API \u4E0D\u53EF\u7528\u6216\u8D85\u65F6\uFF09\u3002");
374
312
  }
375
313
  return;
376
314
  }
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");
315
+ const n = Number(content);
316
+ if (!Number.isInteger(n) || n < 1 || n > state.items.length) return next();
317
+ if (state.timer) clearTimeout(state.timer);
318
+ const tipIds = await safeSend(session, config.generationTip);
319
+ state.tipMessageIds.push(...tipIds);
320
+ if (config.tipRecallSec > 0 && !config.recallOnlyAfterSuccess) recall(session, tipIds, config.tipRecallSec);
321
+ try {
322
+ const item = state.items[n - 1];
323
+ const songUrl = await apiGetSongUrl(ctx, config, item);
324
+ if (config.maxSongDuration > 0 && item.duration && item.duration / 60 > config.maxSongDuration) {
325
+ await session.send(`\u8BE5\u6B4C\u66F2\u65F6\u957F\u8D85\u51FA\u9650\u5236\uFF08>${config.maxSongDuration} \u5206\u949F\uFF09\uFF0C\u5DF2\u53D6\u6D88\u53D1\u9001\u3002`);
381
326
  return;
382
327
  }
383
- state.page -= 1;
384
- 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");
328
+ await sendSong(session, ctx, config, songUrl);
329
+ if (config.recallOnlyAfterSuccess) {
330
+ if (config.tipRecallSec > 0) recall(session, tipIds, 1);
331
+ if (config.menuRecallSec > 0) recall(session, state.menuMessageIds, 1);
332
+ }
333
+ pending.delete(k);
334
+ return;
335
+ } catch (e) {
336
+ logger.warn("send failed: %s", e.stack || e.message);
337
+ await session.send("\u83B7\u53D6/\u53D1\u9001\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
338
+ if (!config.keepMenuIfSendFailed) {
339
+ pending.delete(k);
340
+ } else {
341
+ state.timer = setTimeout(() => pending.delete(k), ms(config.waitForTimeout));
389
342
  }
390
343
  return;
391
344
  }
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
345
  });
397
346
  }
398
347
  // Annotate the CommonJS export names for ESM import in node:
399
348
  0 && (module.exports = {
400
349
  Config,
401
350
  apply,
402
- name
351
+ inject,
352
+ name,
353
+ usage
403
354
  });