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.js
CHANGED
|
@@ -38,345 +38,307 @@ __export(index_exports, {
|
|
|
38
38
|
});
|
|
39
39
|
module.exports = __toCommonJS(index_exports);
|
|
40
40
|
var import_koishi = require("koishi");
|
|
41
|
-
var import_node_os = __toESM(require("os"));
|
|
42
41
|
var import_node_fs = __toESM(require("fs"));
|
|
43
|
-
var import_node_crypto = __toESM(require("crypto"));
|
|
44
42
|
var import_node_path = __toESM(require("path"));
|
|
45
|
-
var
|
|
43
|
+
var import_node_os = __toESM(require("os"));
|
|
44
|
+
var import_node_crypto = __toESM(require("crypto"));
|
|
45
|
+
var import_node_child_process = require("child_process");
|
|
46
46
|
var name = "music-to-voice";
|
|
47
47
|
var logger = new import_koishi.Logger(name);
|
|
48
48
|
var inject = {
|
|
49
|
-
|
|
50
|
-
optional: ["puppeteer", "downloads", "ffmpeg", "silk"]
|
|
49
|
+
optional: ["downloads", "ffmpeg", "silk", "puppeteer"]
|
|
51
50
|
};
|
|
52
51
|
var usage = `
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
\u672C\u63D2\u4EF6\u63D0\u4F9B\u201C\u70B9\u6B4C \u2192 \u5217\u8868\u9009\u5E8F\u53F7 \u2192 \u53D1\u9001\u8BED\u97F3/\u97F3\u9891/\u6587\u4EF6\u201D\u7684\u97F3\u4E50\u805A\u5408\u80FD\u529B\u3002
|
|
52
|
+
### \u70B9\u6B4C\u8BED\u97F3\uFF08\u652F\u6301\u7FFB\u9875 + \u53EF\u9009 silk/ffmpeg\uFF09
|
|
56
53
|
|
|
57
|
-
\
|
|
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
|
|
58
55
|
|
|
59
|
-
|
|
56
|
+
- **puppeteer \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09**
|
|
60
57
|
|
|
61
|
-
|
|
58
|
+
\u6B64\u5916\u53EF\u80FD\u8FD8\u9700\u8981\u8FD9\u4E9B\u670D\u52A1\u624D\u80FD\u53D1\u9001\u8BED\u97F3\uFF1A
|
|
62
59
|
|
|
63
|
-
\
|
|
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**
|
|
64
62
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
\u6B64\u5916\u53EF\u80FD\u8FD8\u9700\u8981\u8FD9\u4E9B\u670D\u52A1\u624D\u80FD\u53D1\u9001\u8BED\u97F3\uFF08\u6700\u7A33\uFF1A\u4E0B\u8F7D + \u8F6C\u7801\uFF09\uFF1A
|
|
68
|
-
|
|
69
|
-
- downloads \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09
|
|
70
|
-
- ffmpeg \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF0C\u53EF\u80FD\u4F9D\u8D56 downloads\uFF09
|
|
71
|
-
- silk \u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09
|
|
72
|
-
|
|
73
|
-
---
|
|
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
|
|
74
64
|
`;
|
|
75
|
-
var Config = import_koishi.Schema.
|
|
76
|
-
import_koishi.Schema.
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
import_koishi.Schema.
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
import_koishi.Schema.
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
import_koishi.Schema.
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
import_koishi.Schema.
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
import_koishi.Schema.object({
|
|
113
|
-
recallMenu: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\u6B4C\u5355\u6D88\u606F"),
|
|
114
|
-
recallGeneratingTip: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\u751F\u6210\u63D0\u793A\u6D88\u606F"),
|
|
115
|
-
recallSongMessage: import_koishi.Schema.boolean().default(false).description("\u64A4\u56DE\u6B4C\u66F2\u6D88\u606F\uFF08\u8BED\u97F3/\u97F3\u9891/\u6587\u4EF6\uFF09"),
|
|
116
|
-
recallTimeoutTip: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\u8D85\u65F6\u63D0\u793A"),
|
|
117
|
-
recallExitTip: import_koishi.Schema.boolean().default(false).description("\u64A4\u56DE\u9000\u51FA\u63D0\u793A"),
|
|
118
|
-
recallInvalidTip: import_koishi.Schema.boolean().default(false).description("\u64A4\u56DE\u5E8F\u53F7\u9519\u8BEF\u63D0\u793A"),
|
|
119
|
-
recallFetchFailTip: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\u83B7\u53D6\u5931\u8D25\u63D0\u793A")
|
|
120
|
-
}).description("\u64A4\u56DE\u8BBE\u7F6E"),
|
|
121
|
-
import_koishi.Schema.object({
|
|
122
|
-
enableRateLimit: import_koishi.Schema.boolean().default(false).description("\u662F\u5426\u5F00\u542F\u9891\u7387\u9650\u5236"),
|
|
123
|
-
rateLimitScope: import_koishi.Schema.union([
|
|
124
|
-
import_koishi.Schema.const("user").description("\u6309\u7528\u6237"),
|
|
125
|
-
import_koishi.Schema.const("channel").description("\u6309\u9891\u9053"),
|
|
126
|
-
import_koishi.Schema.const("platform").description("\u6309\u5E73\u53F0")
|
|
127
|
-
]).default("user").description("\u9650\u5236\u8303\u56F4"),
|
|
128
|
-
rateLimitIntervalSec: import_koishi.Schema.natural().min(1).max(3600).default(60).description("\u95F4\u9694\u79D2\u6570")
|
|
129
|
-
}).description("\u9891\u7387\u9650\u5236")
|
|
130
|
-
]);
|
|
131
|
-
function artistsToString(x) {
|
|
132
|
-
if (!x) return "";
|
|
133
|
-
if (Array.isArray(x)) return x.join(" / ");
|
|
134
|
-
return String(x);
|
|
65
|
+
var Config = import_koishi.Schema.object({
|
|
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
|
+
// ✅ 后台显示品牌名(你要求的)
|
|
70
|
+
source: import_koishi.Schema.union([
|
|
71
|
+
import_koishi.Schema.const("netease").description("\u7F51\u6613\u4E91"),
|
|
72
|
+
import_koishi.Schema.const("tencent").description("QQ\u97F3\u4E50"),
|
|
73
|
+
import_koishi.Schema.const("kugou").description("\u9177\u72D7"),
|
|
74
|
+
import_koishi.Schema.const("kuwo").description("\u9177\u6211"),
|
|
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),
|
|
88
|
+
sendAs: import_koishi.Schema.union([
|
|
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;
|
|
135
102
|
}
|
|
136
|
-
|
|
137
|
-
|
|
103
|
+
function keyOf(session) {
|
|
104
|
+
return `${session.platform}:${session.userId || "unknown"}:${session.channelId || session.guildId || "unknown"}`;
|
|
105
|
+
}
|
|
106
|
+
function normalizeArtist(a) {
|
|
107
|
+
if (!a) return "";
|
|
108
|
+
if (Array.isArray(a)) return a.join(" / ");
|
|
109
|
+
return String(a);
|
|
110
|
+
}
|
|
111
|
+
function formatMenu(state, config) {
|
|
112
|
+
const lines = [];
|
|
113
|
+
lines.push(`\u70B9\u6B4C\u5217\u8868\uFF08\u7B2C ${state.page} \u9875\uFF09`);
|
|
114
|
+
lines.push(`\u5173\u952E\u8BCD\uFF1A${state.keyword}`);
|
|
115
|
+
lines.push("");
|
|
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
|
+
});
|
|
121
|
+
lines.push("");
|
|
122
|
+
lines.push(`\u8BF7\u5728 ${config.waitForTimeout} \u79D2\u5185\u8F93\u5165\u6B4C\u66F2\u5E8F\u53F7`);
|
|
123
|
+
lines.push(`\u7FFB\u9875\uFF1A${config.prevPageCommand} / ${config.nextPageCommand}`);
|
|
124
|
+
if (config.menuExitCommandTip) lines.push(`\u9000\u51FA\uFF1A${config.exitCommandList.join(" / ")}`);
|
|
125
|
+
return lines.join("\n");
|
|
126
|
+
}
|
|
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;
|
|
138
134
|
const channelId = session.channelId;
|
|
139
135
|
if (!channelId) return;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
}
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
ids.forEach((id) => session.bot.deleteMessage(channelId, id).catch(() => {
|
|
138
|
+
}));
|
|
139
|
+
}, sec * 1e3);
|
|
144
140
|
}
|
|
145
|
-
function
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
141
|
+
async function apiSearch(ctx, config, keyword, page) {
|
|
142
|
+
const params = new URLSearchParams({
|
|
143
|
+
types: "search",
|
|
144
|
+
source: config.source,
|
|
145
|
+
name: keyword,
|
|
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
|
|
155
154
|
});
|
|
156
|
-
if (!Array.isArray(data))
|
|
157
|
-
return data
|
|
158
|
-
id: String(x.id ?? x.url_id ?? ""),
|
|
159
|
-
name: String(x.name ?? ""),
|
|
160
|
-
artist: artistsToString(x.artist),
|
|
161
|
-
album: String(x.album ?? ""),
|
|
162
|
-
source: String(x.source ?? config.source)
|
|
163
|
-
})).filter((x) => x.id && x.name);
|
|
155
|
+
if (!Array.isArray(data)) throw new Error("search response is not array");
|
|
156
|
+
return data;
|
|
164
157
|
}
|
|
165
|
-
async function
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
|
170
164
|
});
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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;
|
|
175
174
|
}
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const ext = ".mp3";
|
|
180
|
-
const fp = import_node_path.default.join(import_node_os.default.tmpdir(), `music_${import_node_crypto.default.randomBytes(8).toString("hex")}${ext}`);
|
|
181
|
-
import_node_fs.default.writeFileSync(fp, Buffer.from(file.data));
|
|
182
|
-
return fp;
|
|
183
|
-
} catch (e) {
|
|
184
|
-
return null;
|
|
185
|
-
}
|
|
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}`);
|
|
186
178
|
}
|
|
187
|
-
async function
|
|
179
|
+
async function downloadToFile(ctx, config, url, filePath) {
|
|
188
180
|
const anyCtx = ctx;
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
await anyCtx.silk.encode(wav, silk);
|
|
195
|
-
return silk;
|
|
196
|
-
} catch {
|
|
197
|
-
return localPath;
|
|
198
|
-
} finally {
|
|
199
|
-
try {
|
|
200
|
-
import_node_fs.default.existsSync(wav) && import_node_fs.default.unlinkSync(wav);
|
|
201
|
-
} catch {
|
|
202
|
-
}
|
|
181
|
+
if (anyCtx.downloads?.download) {
|
|
182
|
+
await anyCtx.downloads.download(url, filePath, {
|
|
183
|
+
headers: { "user-agent": config.userAgent }
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
203
186
|
}
|
|
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));
|
|
193
|
+
}
|
|
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
|
+
});
|
|
204
200
|
}
|
|
205
|
-
async function
|
|
201
|
+
async function encodeSilk(ctx, pcmPath) {
|
|
206
202
|
const anyCtx = ctx;
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
await page.close();
|
|
214
|
-
return buf;
|
|
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);
|
|
207
|
+
}
|
|
208
|
+
throw new Error("silk service encode not available");
|
|
215
209
|
}
|
|
216
|
-
function
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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 {
|
|
227
|
+
}
|
|
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);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
225
233
|
}
|
|
226
|
-
return
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
\u8BF7\u5728 ${config.waitForTimeout} \u79D2\u5185\u8F93\u5165\u5E8F\u53F7\uFF1A`;
|
|
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));
|
|
230
237
|
}
|
|
231
238
|
function apply(ctx, config) {
|
|
232
|
-
|
|
233
|
-
ctx.command(`${config.commandName} <keyword:text>`, "\u97F3\u4E50\u805A\u5408\u70B9\u6B4C\u5E76\u53D1\u9001\u8BED\u97F3").alias(config.commandAlias).action(async (argv, keyword) => {
|
|
234
|
-
const session = argv.session;
|
|
235
|
-
const options = argv.options ?? {};
|
|
239
|
+
ctx.command(`${config.commandName} <keyword:text>`, "\u70B9\u6B4C\u5E76\u53D1\u9001\u8BED\u97F3/\u97F3\u9891").alias(config.commandAlias).action(async ({ session }, keyword) => {
|
|
236
240
|
if (!session) return;
|
|
237
|
-
keyword = (keyword
|
|
241
|
+
keyword = (keyword || "").trim();
|
|
238
242
|
if (!keyword) return `\u7528\u6CD5\uFF1A${config.commandName} \u6B4C\u66F2\u540D`;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
lastUse.set(key, now);
|
|
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;
|
|
248
|
+
try {
|
|
249
|
+
items = await apiSearch(ctx, config, keyword, 1);
|
|
250
|
+
} catch (e) {
|
|
251
|
+
logger.warn("search failed: %s", e.message);
|
|
252
|
+
return "\u641C\u7D22\u5931\u8D25\uFF08API \u4E0D\u53EF\u7528\u6216\u8D85\u65F6\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002";
|
|
250
253
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
254
|
+
if (!items.length) return "\u6CA1\u6709\u641C\u7D22\u5230\u7ED3\u679C\u3002";
|
|
255
|
+
const state = {
|
|
256
|
+
userId: session.userId || "",
|
|
257
|
+
channelId: session.channelId || "",
|
|
258
|
+
keyword,
|
|
259
|
+
page: 1,
|
|
260
|
+
items,
|
|
261
|
+
menuMessageIds: [],
|
|
262
|
+
tipMessageIds: []
|
|
263
|
+
};
|
|
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);
|
|
269
|
+
}
|
|
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;
|
|
277
|
+
});
|
|
278
|
+
ctx.middleware(async (session, next) => {
|
|
279
|
+
const k = keyOf(session);
|
|
280
|
+
const state = pending.get(k);
|
|
281
|
+
if (!state) return next();
|
|
282
|
+
if ((session.userId || "") !== state.userId || (session.channelId || "") !== state.channelId) return next();
|
|
283
|
+
const content = (session.content || "").trim();
|
|
284
|
+
if (!content) return next();
|
|
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");
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
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");
|
|
268
295
|
return;
|
|
269
296
|
}
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
body{margin:0;background:${config.backgroundColor};color:${config.textColor};font-size:16px;}
|
|
275
|
-
#song-list{padding:18px;white-space:nowrap;}
|
|
276
|
-
</style></head><body><div id="song-list">${text2}</div></body></html>`;
|
|
277
|
-
const buf = await renderListImage(ctx, html);
|
|
278
|
-
if (!buf) {
|
|
279
|
-
const ids2 = await session.send("\u56FE\u7247\u6B4C\u5355\u751F\u6210\u5931\u8D25\uFF1A\u672A\u5B89\u88C5 puppeteer \u670D\u52A1\u6216\u5176\u4E0D\u53EF\u7528\u3002");
|
|
280
|
-
const mid = Array.isArray(ids2) ? ids2[0] : ids2;
|
|
281
|
-
if (config.recallFetchFailTip) await safeDelete(session, mid);
|
|
297
|
+
try {
|
|
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");
|
|
282
301
|
return;
|
|
283
302
|
}
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
const input = await session.prompt(
|
|
292
|
-
(s) => {
|
|
293
|
-
const els = s.elements ?? [];
|
|
294
|
-
return import_koishi.h.select(els, "text").join("");
|
|
295
|
-
},
|
|
296
|
-
{ timeout: config.waitForTimeout * 1e3 }
|
|
297
|
-
);
|
|
298
|
-
if ((0, import_koishi.isNullable)(input)) {
|
|
299
|
-
if (config.recallMenu) await safeDelete(session, menuMsgId);
|
|
300
|
-
const ids = await session.send("\u8F93\u5165\u8D85\u65F6\uFF0C\u5DF2\u53D6\u6D88\u70B9\u6B4C\u3002");
|
|
301
|
-
const mid = Array.isArray(ids) ? ids[0] : ids;
|
|
302
|
-
if (config.recallTimeoutTip) await safeDelete(session, mid);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
const text = String(input).trim();
|
|
306
|
-
if (!text) continue;
|
|
307
|
-
if (config.exitCommandList.includes(text)) {
|
|
308
|
-
if (config.recallMenu) await safeDelete(session, menuMsgId);
|
|
309
|
-
const ids = await session.send("\u5DF2\u9000\u51FA\u6B4C\u66F2\u9009\u62E9\u3002");
|
|
310
|
-
const mid = Array.isArray(ids) ? ids[0] : ids;
|
|
311
|
-
if (config.recallExitTip) await safeDelete(session, mid);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
if (text === config.nextPageCommand) {
|
|
315
|
-
page += 1;
|
|
316
|
-
continue;
|
|
317
|
-
}
|
|
318
|
-
if (text === config.prevPageCommand) {
|
|
319
|
-
page = Math.max(1, page - 1);
|
|
320
|
-
continue;
|
|
321
|
-
}
|
|
322
|
-
if (!/^\d+$/.test(text)) continue;
|
|
323
|
-
const idx = parseInt(text, 10);
|
|
324
|
-
const start = (page - 1) * config.searchListCount;
|
|
325
|
-
const local = idx - start - 1;
|
|
326
|
-
if (local < 0 || local >= list.length) {
|
|
327
|
-
if (config.recallMenu) await safeDelete(session, menuMsgId);
|
|
328
|
-
const ids = await session.send("\u5E8F\u53F7\u8F93\u5165\u9519\u8BEF\uFF0C\u5DF2\u9000\u51FA\u6B4C\u66F2\u9009\u62E9\u3002");
|
|
329
|
-
const mid = Array.isArray(ids) ? ids[0] : ids;
|
|
330
|
-
if (config.recallInvalidTip) await safeDelete(session, mid);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
const chosen = list[local];
|
|
334
|
-
let tipMsgId = null;
|
|
335
|
-
{
|
|
336
|
-
const ids = await session.send(config.generationTip);
|
|
337
|
-
tipMsgId = Array.isArray(ids) ? ids[0] : ids;
|
|
338
|
-
}
|
|
339
|
-
let playUrl = "";
|
|
340
|
-
try {
|
|
341
|
-
playUrl = await gdUrl(ctx, config, chosen);
|
|
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);
|
|
342
309
|
} catch (e) {
|
|
343
|
-
logger.warn(
|
|
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");
|
|
344
312
|
}
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
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`);
|
|
351
326
|
return;
|
|
352
327
|
}
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
if (config.
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
} else {
|
|
368
|
-
sendIds = await session.send([(0, import_koishi.h)("record", { src: finalSrc })]);
|
|
369
|
-
}
|
|
370
|
-
const songMsgId = Array.isArray(sendIds) ? sendIds[0] : sendIds;
|
|
371
|
-
if (config.recallSongMessage) await safeDelete(session, songMsgId);
|
|
372
|
-
} catch (e) {
|
|
373
|
-
logger.warn(`[send] ${String(e?.message || e)}`);
|
|
374
|
-
const ids = await session.send("\u53D1\u9001\u5931\u8D25\uFF08\u53EF\u80FD\u7F3A\u5C11\u8F6C\u7801\u4F9D\u8D56\u6216\u94FE\u63A5\u5931\u6548\uFF09\u3002");
|
|
375
|
-
const mid = Array.isArray(ids) ? ids[0] : ids;
|
|
376
|
-
if (config.recallFetchFailTip) await safeDelete(session, mid);
|
|
377
|
-
} finally {
|
|
378
|
-
if (config.recallGeneratingTip) await safeDelete(session, tipMsgId);
|
|
379
|
-
if (config.recallMenu) await safeDelete(session, menuMsgId);
|
|
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));
|
|
380
342
|
}
|
|
381
343
|
return;
|
|
382
344
|
}
|