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