koishi-plugin-music-to-voice 1.0.0

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/README.md ADDED
@@ -0,0 +1,12 @@
1
+ # 🎵 Koishi 点歌语音插件 (music-voice-pro)
2
+
3
+ 一个适用于 Koishi 机器人的点歌语音插件,支持:
4
+
5
+ - 🔍 搜索歌曲并返回列表
6
+ - 📄 下一页 / 上一页翻页选择
7
+ - 🔢 输入序号点歌
8
+ - 🎤 发送语音(可选 ffmpeg + silk 转码)
9
+ - 🗑 自动撤回菜单 / 提示 / 语音(可配置)
10
+ - 🎛 后台可选择音源
11
+
12
+
@@ -0,0 +1,31 @@
1
+ import { Schema, Context } from 'koishi';
2
+
3
+ declare const name = "music-voice-pro";
4
+ type MusicSource = 'netease' | 'tencent' | 'kugou' | 'kuwo' | 'migu' | 'baidu';
5
+ interface Config {
6
+ commandName: string;
7
+ commandAlias: string;
8
+ apiBase: string;
9
+ source: MusicSource;
10
+ searchListCount: number;
11
+ waitForTimeout: number;
12
+ nextPageCommand: string;
13
+ prevPageCommand: string;
14
+ exitCommandList: string[];
15
+ menuExitCommandTip: boolean;
16
+ imageMode: boolean;
17
+ sendAs: 'record' | 'audio';
18
+ forceTranscode: boolean;
19
+ tempDir: string;
20
+ cacheMinutes: number;
21
+ generationTip: string;
22
+ recallSearchMenuMessage: boolean;
23
+ recallTipMessage: boolean;
24
+ recallUserSelectMessage: boolean;
25
+ recallVoiceMessage: boolean;
26
+ loggerinfo: boolean;
27
+ }
28
+ declare const Config: Schema<Config>;
29
+ declare function apply(ctx: Context, config: Config): void;
30
+
31
+ export { Config, apply, name };
@@ -0,0 +1,31 @@
1
+ import { Schema, Context } from 'koishi';
2
+
3
+ declare const name = "music-voice-pro";
4
+ type MusicSource = 'netease' | 'tencent' | 'kugou' | 'kuwo' | 'migu' | 'baidu';
5
+ interface Config {
6
+ commandName: string;
7
+ commandAlias: string;
8
+ apiBase: string;
9
+ source: MusicSource;
10
+ searchListCount: number;
11
+ waitForTimeout: number;
12
+ nextPageCommand: string;
13
+ prevPageCommand: string;
14
+ exitCommandList: string[];
15
+ menuExitCommandTip: boolean;
16
+ imageMode: boolean;
17
+ sendAs: 'record' | 'audio';
18
+ forceTranscode: boolean;
19
+ tempDir: string;
20
+ cacheMinutes: number;
21
+ generationTip: string;
22
+ recallSearchMenuMessage: boolean;
23
+ recallTipMessage: boolean;
24
+ recallUserSelectMessage: boolean;
25
+ recallVoiceMessage: boolean;
26
+ loggerinfo: boolean;
27
+ }
28
+ declare const Config: Schema<Config>;
29
+ declare function apply(ctx: Context, config: Config): void;
30
+
31
+ export { Config, apply, name };
package/dist/index.js ADDED
@@ -0,0 +1,403 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name2 in all)
10
+ __defProp(target, name2, { get: all[name2], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Config: () => Config,
34
+ apply: () => apply,
35
+ name: () => name
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+ var import_koishi = require("koishi");
39
+ var import_axios = __toESM(require("axios"));
40
+ var import_node_fs = __toESM(require("fs"));
41
+ var import_node_path = __toESM(require("path"));
42
+ var import_node_os = __toESM(require("os"));
43
+ var import_node_crypto = __toESM(require("crypto"));
44
+ var name = "music-voice-pro";
45
+ var logger = new import_koishi.Logger(name);
46
+ var Config = import_koishi.Schema.object({
47
+ commandName: import_koishi.Schema.string().default("\u542C\u6B4C").description("\u6307\u4EE4\u540D\u79F0"),
48
+ commandAlias: import_koishi.Schema.string().default("music").description("\u6307\u4EE4\u522B\u540D"),
49
+ apiBase: import_koishi.Schema.string().default("https://music-api.gdstudio.xyz/api.php").description("\u97F3\u4E50 API \u5730\u5740\uFF08GD\u97F3\u4E50\u53F0 API\uFF09"),
50
+ source: import_koishi.Schema.union([
51
+ import_koishi.Schema.const("netease").description("\u7F51\u6613\u4E91"),
52
+ import_koishi.Schema.const("tencent").description("QQ\u97F3\u4E50"),
53
+ import_koishi.Schema.const("kugou").description("\u9177\u72D7"),
54
+ import_koishi.Schema.const("kuwo").description("\u9177\u6211"),
55
+ import_koishi.Schema.const("migu").description("\u54AA\u5495"),
56
+ import_koishi.Schema.const("baidu").description("\u767E\u5EA6")
57
+ ]).default("netease").description("\u97F3\u6E90\uFF08\u4E0B\u62C9\u9009\u62E9\uFF09"),
58
+ searchListCount: import_koishi.Schema.number().min(5).max(50).default(20).description("\u641C\u7D22\u5217\u8868\u6570\u91CF"),
59
+ waitForTimeout: import_koishi.Schema.number().min(10).max(180).default(45).description("\u7B49\u5F85\u8F93\u5165\u5E8F\u53F7\u8D85\u65F6\uFF08\u79D2\uFF09"),
60
+ nextPageCommand: import_koishi.Schema.string().default("\u4E0B\u4E00\u9875").description("\u4E0B\u4E00\u9875\u6307\u4EE4"),
61
+ prevPageCommand: import_koishi.Schema.string().default("\u4E0A\u4E00\u9875").description("\u4E0A\u4E00\u9875\u6307\u4EE4"),
62
+ exitCommandList: import_koishi.Schema.array(String).default(["0", "\u4E0D\u542C\u4E86", "\u9000\u51FA"]).description("\u9000\u51FA\u6307\u4EE4\u5217\u8868"),
63
+ menuExitCommandTip: import_koishi.Schema.boolean().default(false).description("\u662F\u5426\u5728\u6B4C\u5355\u672B\u5C3E\u63D0\u793A\u9000\u51FA\u6307\u4EE4"),
64
+ imageMode: import_koishi.Schema.boolean().default(false).description("\u56FE\u7247\u6B4C\u5355\u6A21\u5F0F\uFF08\u53EF\u9009\uFF1A\u9700\u8981 puppeteer \u63D2\u4EF6\uFF0C\u5F53\u524D\u4EC5\u4FDD\u7559\u5F00\u5173\uFF09"),
65
+ sendAs: import_koishi.Schema.union([
66
+ import_koishi.Schema.const("record").description("\u8BED\u97F3 record"),
67
+ import_koishi.Schema.const("audio").description("\u97F3\u9891 audio")
68
+ ]).default("record").description("\u53D1\u9001\u7C7B\u578B"),
69
+ forceTranscode: import_koishi.Schema.boolean().default(true).description("\u662F\u5426\u5F3A\u5236\u8F6C\u7801\u4E3A silk\uFF08\u9700\u8981 ffmpeg + silk \u63D2\u4EF6\uFF09"),
70
+ tempDir: import_koishi.Schema.string().default(import_node_path.default.join(import_node_os.default.tmpdir(), "koishi-music-voice")).description("\u4E34\u65F6\u76EE\u5F55"),
71
+ cacheMinutes: import_koishi.Schema.number().min(0).max(1440).default(120).description("\u7F13\u5B58\u65F6\u957F\uFF08\u5206\u949F\uFF0C0=\u4E0D\u7F13\u5B58\uFF09"),
72
+ generationTip: import_koishi.Schema.string().default("\u751F\u6210\u8BED\u97F3\u4E2D...").description("\u7528\u6237\u9009\u6B4C\u540E\u63D0\u793A"),
73
+ recallSearchMenuMessage: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\uFF1A\u6B4C\u5355\u6D88\u606F"),
74
+ recallTipMessage: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\uFF1A\u751F\u6210\u63D0\u793A\u6D88\u606F"),
75
+ recallUserSelectMessage: import_koishi.Schema.boolean().default(true).description("\u64A4\u56DE\uFF1A\u7528\u6237\u8F93\u5165\u7684\u5E8F\u53F7\u6D88\u606F"),
76
+ recallVoiceMessage: import_koishi.Schema.boolean().default(false).description("\u64A4\u56DE\uFF1A\u8BED\u97F3\u6D88\u606F"),
77
+ loggerinfo: import_koishi.Schema.boolean().default(false).description("\u65E5\u5FD7\u8C03\u8BD5\u6A21\u5F0F")
78
+ }).description("\u70B9\u6B4C\u8BED\u97F3\uFF08\u652F\u6301\u7FFB\u9875 + \u53EF\u9009 silk/ffmpeg\uFF09");
79
+ function safeText(x) {
80
+ return typeof x === "string" ? x : x == null ? "" : String(x);
81
+ }
82
+ function normalizeSearchList(data) {
83
+ const arr = Array.isArray(data) ? data : Array.isArray(data?.result) ? data.result : Array.isArray(data?.data) ? data.data : Array.isArray(data?.songs) ? data.songs : [];
84
+ return arr.map((it) => {
85
+ const id = safeText(it?.id ?? it?.songid ?? it?.rid ?? it?.hash ?? it?.mid);
86
+ const name2 = safeText(it?.name ?? it?.songname ?? it?.title);
87
+ const artist = safeText(it?.artist) || safeText(it?.singer) || safeText(it?.author) || (Array.isArray(it?.artists) ? it.artists.map((a) => safeText(a?.name)).filter(Boolean).join("/") : "");
88
+ const album = safeText(it?.album ?? it?.albummid ?? it?.albumname);
89
+ return { id, name: name2, artist, album };
90
+ }).filter((x) => x.id && x.name);
91
+ }
92
+ function normalizeUrl(data) {
93
+ return safeText(data?.url) || safeText(data?.data?.url) || safeText(data?.result?.url) || safeText(data?.data) || "";
94
+ }
95
+ function md5(s) {
96
+ return import_node_crypto.default.createHash("md5").update(s).digest("hex");
97
+ }
98
+ function ensureDir(p) {
99
+ import_node_fs.default.mkdirSync(p, { recursive: true });
100
+ }
101
+ async function tryRecall(session, messageId) {
102
+ if (!messageId) return;
103
+ try {
104
+ await session.bot.deleteMessage(session.channelId, messageId);
105
+ } catch {
106
+ }
107
+ }
108
+ function hRecord(src) {
109
+ return (0, import_koishi.h)("record", { src });
110
+ }
111
+ function hAudio(src) {
112
+ return (0, import_koishi.h)("audio", { src });
113
+ }
114
+ function buildMenuText(config, keyword, list, page) {
115
+ const lines = [];
116
+ lines.push(`NetEase Music:`);
117
+ lines.push(`\u5173\u952E\u8BCD\uFF1A${keyword}`);
118
+ lines.push(`\u97F3\u6E90\uFF1A${config.source} \u7B2C ${page} \u9875`);
119
+ lines.push("");
120
+ for (let i = 0; i < list.length; i++) {
121
+ const it = list[i];
122
+ const meta = [it.artist, it.album].filter(Boolean).join(" - ");
123
+ lines.push(`${i + 1}. ${it.name}${meta ? ` -- ${meta}` : ""}`);
124
+ }
125
+ lines.push("");
126
+ lines.push(`\u8BF7\u5728 ${config.waitForTimeout} \u79D2\u5185\u8F93\u5165\u5E8F\u53F7\uFF081-${list.length}\uFF09`);
127
+ 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");
133
+ return lines.join("\n");
134
+ }
135
+ async function apiSearch(config, keyword, page = 1) {
136
+ const params = {
137
+ types: "search",
138
+ source: config.source,
139
+ name: keyword,
140
+ count: config.searchListCount,
141
+ pages: page
142
+ };
143
+ const { data } = await import_axios.default.get(config.apiBase, { params, timeout: 15e3 });
144
+ return normalizeSearchList(data);
145
+ }
146
+ async function apiGetSongUrl(config, id) {
147
+ const params = { types: "url", source: config.source, id };
148
+ const { data } = await import_axios.default.get(config.apiBase, { params, timeout: 15e3 });
149
+ return normalizeUrl(data);
150
+ }
151
+ async function downloadToFile(url, filePath) {
152
+ const res = await import_axios.default.get(url, { responseType: "stream", timeout: 3e4 });
153
+ await new Promise((resolve, reject) => {
154
+ const ws = import_node_fs.default.createWriteStream(filePath);
155
+ res.data.pipe(ws);
156
+ ws.on("finish", () => resolve());
157
+ ws.on("error", reject);
158
+ });
159
+ }
160
+ async function sendVoiceByUrl(session, config, audioUrl) {
161
+ const seg = config.sendAs === "record" ? hRecord(audioUrl) : hAudio(audioUrl);
162
+ const ids = await session.send(seg);
163
+ return Array.isArray(ids) ? ids[0] : ids;
164
+ }
165
+ async function sendVoiceByFile(session, config, absPath) {
166
+ const url = `file://${absPath.replace(/\\/g, "/")}`;
167
+ const seg = config.sendAs === "record" ? hRecord(url) : hAudio(url);
168
+ const ids = await session.send(seg);
169
+ return Array.isArray(ids) ? ids[0] : ids;
170
+ }
171
+ async function buildSilkIfPossible(ctx, config, audioUrl, cacheKey) {
172
+ const hasFfmpeg = !!ctx.ffmpeg;
173
+ const hasSilk = !!ctx.silk;
174
+ if (!hasFfmpeg || !hasSilk) return null;
175
+ ensureDir(config.tempDir);
176
+ const silkPath = import_node_path.default.join(config.tempDir, `${cacheKey}.silk`);
177
+ if (config.cacheMinutes > 0 && import_node_fs.default.existsSync(silkPath)) {
178
+ return silkPath;
179
+ }
180
+ const rawPath = import_node_path.default.join(config.tempDir, `${cacheKey}.src`);
181
+ await downloadToFile(audioUrl, rawPath);
182
+ const wavPath = import_node_path.default.join(config.tempDir, `${cacheKey}.wav`);
183
+ const ffmpeg = ctx.ffmpeg;
184
+ try {
185
+ if (typeof ffmpeg.convert === "function") {
186
+ await ffmpeg.convert(rawPath, wavPath, {
187
+ format: "wav",
188
+ audioChannels: 1,
189
+ audioFrequency: 24e3
190
+ });
191
+ } else if (typeof ffmpeg.exec === "function") {
192
+ await ffmpeg.exec(["-y", "-i", rawPath, "-ac", "1", "-ar", "24000", wavPath]);
193
+ } else {
194
+ throw new Error("ffmpeg service API not recognized");
195
+ }
196
+ } catch (e) {
197
+ throw new Error(`ffmpeg \u8F6C\u7801\u5931\u8D25\uFF1A${e?.message || String(e)}`);
198
+ }
199
+ const silk = ctx.silk;
200
+ try {
201
+ if (typeof silk.encode === "function") {
202
+ await silk.encode(wavPath, silkPath, { rate: 24e3 });
203
+ } else if (typeof silk.encodeWav === "function") {
204
+ await silk.encodeWav(wavPath, silkPath, { rate: 24e3 });
205
+ } else {
206
+ throw new Error("silk service API not recognized");
207
+ }
208
+ } catch (e) {
209
+ throw new Error(`silk \u7F16\u7801\u5931\u8D25\uFF1A${e?.message || String(e)}`);
210
+ } finally {
211
+ try {
212
+ import_node_fs.default.unlinkSync(rawPath);
213
+ } catch {
214
+ }
215
+ try {
216
+ import_node_fs.default.unlinkSync(wavPath);
217
+ } catch {
218
+ }
219
+ }
220
+ return silkPath;
221
+ }
222
+ function logDepsHint(ctx) {
223
+ const hasPuppeteer = !!ctx.puppeteer;
224
+ const hasFfmpeg = !!ctx.ffmpeg;
225
+ const hasSilk = !!ctx.silk;
226
+ const hasDownloads = !!ctx.downloads;
227
+ logger.info("\u5F00\u542F\u63D2\u4EF6\u524D\uFF0C\u8BF7\u786E\u4FDD\u4EE5\u4E0B\u670D\u52A1\u5DF2\u7ECF\u542F\u7528\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A");
228
+ logger.info(`- puppeteer\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A${hasPuppeteer ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
229
+ logger.info("\u6B64\u5916\u53EF\u80FD\u8FD8\u9700\u8981\u8FD9\u4E9B\u670D\u52A1\u624D\u80FD\u53D1\u9001\u8BED\u97F3\uFF1A");
230
+ logger.info(`- ffmpeg\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF08\u6B64\u670D\u52A1\u53EF\u80FD\u989D\u5916\u4F9D\u8D56downloads\u670D\u52A1\uFF09\uFF1A${hasFfmpeg ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
231
+ logger.info(`- silk\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A${hasSilk ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
232
+ logger.info(`- downloads\u670D\u52A1\uFF08\u53EF\u9009\u5B89\u88C5\uFF09\uFF1A${hasDownloads ? "\u5DF2\u68C0\u6D4B\u5230" : "\u672A\u68C0\u6D4B\u5230"}`);
233
+ logger.info("Music API \u51FA\u5904\uFF1AGD\u97F3\u4E50\u53F0 API\uFF08https://music-api.gdstudio.xyz/api.php\uFF09");
234
+ }
235
+ function apply(ctx, config) {
236
+ if (config.loggerinfo) logger.level = import_koishi.Logger.DEBUG;
237
+ logDepsHint(ctx);
238
+ const pending = /* @__PURE__ */ new Map();
239
+ function getKey(session) {
240
+ return String(session?.channelId || "");
241
+ }
242
+ function isExit(input) {
243
+ const t = input.trim();
244
+ return config.exitCommandList.map((s) => s.trim()).includes(t);
245
+ }
246
+ function isExpired(state) {
247
+ return Date.now() > state.expiresAt;
248
+ }
249
+ async function refreshMenu(session, state) {
250
+ const list = await apiSearch(config, state.keyword, state.page);
251
+ state.list = list;
252
+ state.expiresAt = Date.now() + config.waitForTimeout * 1e3;
253
+ if (config.recallSearchMenuMessage) {
254
+ await tryRecall(session, state.menuMessageId);
255
+ state.menuMessageId = void 0;
256
+ }
257
+ if (!config.recallSearchMenuMessage) {
258
+ const text = buildMenuText(config, state.keyword, list, state.page);
259
+ const ids = await session.send(text);
260
+ state.menuMessageId = Array.isArray(ids) ? ids[0] : ids;
261
+ } else {
262
+ await session.send(`\u5DF2\u7FFB\u5230\u7B2C ${state.page} \u9875\uFF0C\u8BF7\u76F4\u63A5\u53D1\u9001\u5E8F\u53F7\uFF081-${list.length}\uFF09`);
263
+ }
264
+ }
265
+ async function handlePick(session, state, pickIndex) {
266
+ const item = state.list[pickIndex];
267
+ if (!item) {
268
+ await session.send(`\u5E8F\u53F7\u65E0\u6548\uFF0C\u8BF7\u8F93\u5165 1-${state.list.length}\uFF0C\u6216\u8F93\u5165 ${config.exitCommandList.join("/")} \u9000\u51FA\u3002`);
269
+ return;
270
+ }
271
+ let tipId;
272
+ if (!config.recallTipMessage && config.generationTip?.trim()) {
273
+ const ids = await session.send(config.generationTip);
274
+ tipId = Array.isArray(ids) ? ids[0] : ids;
275
+ }
276
+ let songUrl = "";
277
+ try {
278
+ songUrl = await apiGetSongUrl(config, item.id);
279
+ if (!songUrl) throw new Error("empty url");
280
+ } catch {
281
+ await session.send("\u83B7\u53D6\u6B4C\u66F2\u76F4\u94FE\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\uFF0C\u6216\u66F4\u6362\u97F3\u6E90\u3002");
282
+ return;
283
+ }
284
+ const cacheKey = md5(`${config.source}:${item.id}`);
285
+ let voiceId;
286
+ try {
287
+ const silkPath = await buildSilkIfPossible(ctx, config, songUrl, cacheKey);
288
+ if (silkPath) {
289
+ voiceId = await sendVoiceByFile(session, config, silkPath);
290
+ } else {
291
+ if (config.forceTranscode) {
292
+ await session.send(
293
+ `\u5F53\u524D\u914D\u7F6E\u4E3A\u3010\u5F3A\u5236 silk \u8F6C\u7801\u3011\u4F46\u672A\u68C0\u6D4B\u5230 ffmpeg/silk \u670D\u52A1\u3002
294
+ \u8BF7\u5728 Koishi \u63D2\u4EF6\u5E02\u573A\u5B89\u88C5\u5E76\u542F\u7528\uFF1Affmpeg\u3001silk\uFF08\u53EF\u80FD\u8FD8\u9700\u8981 downloads\uFF09\u3002`
295
+ );
296
+ return;
297
+ }
298
+ voiceId = await sendVoiceByUrl(session, config, songUrl);
299
+ }
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
+ }
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
+ }
313
+ ctx.command(`${config.commandName} <keyword:text>`, "\u70B9\u6B4C\u5E76\u53D1\u9001\u8BED\u97F3\uFF08GD\u97F3\u4E50\u53F0 API\uFF09").alias(config.commandAlias).action(async ({ session }, keyword) => {
314
+ if (!session) return;
315
+ keyword = (keyword || "").trim();
316
+ if (!keyword) return `\u7528\u6CD5\uFF1A${config.commandName} \u6B4C\u66F2\u540D`;
317
+ const userId = session.userId;
318
+ const channelId = session.channelId;
319
+ if (!userId || !channelId) {
320
+ await session.send("\u5F53\u524D\u9002\u914D\u5668\u672A\u63D0\u4F9B userId/channelId\uFF0C\u65E0\u6CD5\u8FDB\u5165\u9009\u6B4C\u6A21\u5F0F\u3002");
321
+ return;
322
+ }
323
+ let list = [];
324
+ try {
325
+ list = await apiSearch(config, keyword, 1);
326
+ } catch {
327
+ return "\u641C\u7D22\u5931\u8D25\uFF08API \u4E0D\u53EF\u7528\u6216\u8D85\u65F6\uFF09\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002";
328
+ }
329
+ if (!list.length) return "\u6CA1\u6709\u641C\u5230\u7ED3\u679C\uFF0C\u6362\u4E2A\u5173\u952E\u8BCD\u8BD5\u8BD5\u3002";
330
+ const state = {
331
+ userId,
332
+ channelId,
333
+ guildId: session.guildId,
334
+ keyword,
335
+ page: 1,
336
+ list,
337
+ expiresAt: Date.now() + config.waitForTimeout * 1e3
338
+ };
339
+ if (!config.recallSearchMenuMessage) {
340
+ const text = buildMenuText(config, keyword, list, 1);
341
+ const ids = await session.send(text);
342
+ state.menuMessageId = Array.isArray(ids) ? ids[0] : ids;
343
+ } else {
344
+ await session.send(`\u5DF2\u8FDB\u5165\u9009\u6B4C\u6A21\u5F0F\uFF0C\u8BF7\u76F4\u63A5\u53D1\u9001\u5E8F\u53F7\uFF081-${list.length}\uFF09\uFF0C\u6216\u53D1\u9001\u201C${config.nextPageCommand}/${config.prevPageCommand}\u201D\u7FFB\u9875\u3002`);
345
+ }
346
+ pending.set(channelId, state);
347
+ });
348
+ ctx.middleware(async (session, next) => {
349
+ const key = getKey(session);
350
+ if (!key) return next();
351
+ const state = pending.get(key);
352
+ if (!state) return next();
353
+ if (session.userId !== state.userId) return next();
354
+ if (isExpired(state)) {
355
+ pending.delete(key);
356
+ return next();
357
+ }
358
+ const content = (session.content || "").trim();
359
+ if (!content) return next();
360
+ if (isExit(content)) {
361
+ if (config.recallSearchMenuMessage) await tryRecall(session, state.menuMessageId);
362
+ pending.delete(key);
363
+ if (!config.recallUserSelectMessage) await session.send("\u5DF2\u9000\u51FA\u9009\u6B4C\u3002");
364
+ return;
365
+ }
366
+ if (content === config.nextPageCommand) {
367
+ if (config.recallUserSelectMessage) await tryRecall(session, session.messageId);
368
+ state.page += 1;
369
+ try {
370
+ await refreshMenu(session, state);
371
+ } catch {
372
+ state.page -= 1;
373
+ await session.send("\u7FFB\u9875\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
374
+ }
375
+ return;
376
+ }
377
+ if (content === config.prevPageCommand) {
378
+ if (config.recallUserSelectMessage) await tryRecall(session, session.messageId);
379
+ if (state.page <= 1) {
380
+ await session.send("\u5DF2\u7ECF\u662F\u7B2C\u4E00\u9875\u3002");
381
+ return;
382
+ }
383
+ state.page -= 1;
384
+ try {
385
+ await refreshMenu(session, state);
386
+ } catch {
387
+ state.page += 1;
388
+ await session.send("\u7FFB\u9875\u5931\u8D25\uFF0C\u8BF7\u7A0D\u540E\u518D\u8BD5\u3002");
389
+ }
390
+ return;
391
+ }
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
+ });
397
+ }
398
+ // Annotate the CommonJS export names for ESM import in node:
399
+ 0 && (module.exports = {
400
+ Config,
401
+ apply,
402
+ name
403
+ });