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