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.d.mts +24 -13
- package/dist/index.d.ts +24 -13
- package/dist/index.js +247 -296
- package/dist/index.mjs +245 -296
- package/package.json +5 -3
- package/src/index.ts +296 -384
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
|
-
|
|
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
|
|
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().
|
|
48
|
-
commandAlias: import_koishi.Schema.string().
|
|
49
|
-
apiBase: import_koishi.Schema.string().
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
109
|
-
return
|
|
103
|
+
function keyOf(session) {
|
|
104
|
+
return `${session.platform}:${session.userId || "unknown"}:${session.channelId || session.guildId || "unknown"}`;
|
|
110
105
|
}
|
|
111
|
-
function
|
|
112
|
-
|
|
106
|
+
function normalizeArtist(a) {
|
|
107
|
+
if (!a) return "";
|
|
108
|
+
if (Array.isArray(a)) return a.join(" / ");
|
|
109
|
+
return String(a);
|
|
113
110
|
}
|
|
114
|
-
function
|
|
111
|
+
function formatMenu(state, config) {
|
|
115
112
|
const lines = [];
|
|
116
|
-
lines.push(`
|
|
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
|
-
|
|
121
|
-
const
|
|
122
|
-
const
|
|
123
|
-
lines.push(`${
|
|
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
|
|
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
|
|
136
|
-
const
|
|
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
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
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
|
|
161
|
-
const
|
|
162
|
-
const
|
|
163
|
-
|
|
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
|
-
|
|
166
|
-
const
|
|
167
|
-
|
|
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
|
|
172
|
-
const
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
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
|
-
|
|
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
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
return
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
318
|
-
const
|
|
319
|
-
if (
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
|
|
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 (!
|
|
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
|
-
|
|
337
|
-
|
|
260
|
+
items,
|
|
261
|
+
menuMessageIds: [],
|
|
262
|
+
tipMessageIds: []
|
|
338
263
|
};
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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
|
-
|
|
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
|
|
350
|
-
|
|
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 (
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
368
|
-
state.page
|
|
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
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
-
|
|
351
|
+
inject,
|
|
352
|
+
name,
|
|
353
|
+
usage
|
|
403
354
|
});
|