koishi-plugin-comic 1.0.7 → 1.0.9
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/lib/comic-api.d.ts +70 -0
- package/lib/comic-api.d.ts.map +1 -0
- package/lib/comic-api.js +110 -0
- package/lib/comic-api.js.map +1 -0
- package/lib/index.d.ts +1 -27
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +386 -387
- package/lib/index.js.map +1 -1
- package/lib/tools.d.ts +112 -0
- package/lib/tools.d.ts.map +1 -0
- package/lib/tools.js +317 -0
- package/lib/tools.js.map +1 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -53,23 +53,21 @@ exports.usage = `
|
|
|
53
53
|
<h3>命令列表</h3>
|
|
54
54
|
<ul>
|
|
55
55
|
<li><code>comic [关键词]</code> - 聚合搜索/直接下载漫画</li>
|
|
56
|
-
<li><code>comic search <
|
|
56
|
+
<li><code>comic search <关键词/ID></code> - 聚合搜索漫画,各平台最多8条,合并转发并标注来源</li>
|
|
57
57
|
<li><code>comic download <ID> [章节ID]</code> - 下载漫画PDF</li>
|
|
58
|
-
<li><code>comic detail <关键词/ID></code> -
|
|
59
|
-
<li><code>comic leaderboard [类型]</code> -
|
|
60
|
-
<li><code>comic latest</code> -
|
|
61
|
-
<li><code>comic random</code> -
|
|
58
|
+
<li><code>comic detail <关键词/ID></code> - 查看漫画详情,关键词模式下双平台各展示最相似结果</li>
|
|
59
|
+
<li><code>comic leaderboard [类型] [页码]</code> - 排行榜(类型: day/week/month/total,默认day),双平台分2条发送</li>
|
|
60
|
+
<li><code>comic latest [-n 数量]</code> - 最近更新,每平台默认10条(最多50),双平台分2条发送</li>
|
|
61
|
+
<li><code>comic random [-n 数量]</code> - 随机推荐,每平台默认5个(最多20),双平台分2条发送</li>
|
|
62
62
|
</ul>
|
|
63
63
|
`;
|
|
64
64
|
exports.Config = koishi_1.Schema.object({
|
|
65
65
|
apiBase: koishi_1.Schema.string()
|
|
66
66
|
.description('comic-api 后端地址')
|
|
67
67
|
.default('http://127.0.0.1:8699'),
|
|
68
|
-
defaultSource: koishi_1.Schema.union(['jm', 'bika'])
|
|
69
|
-
.description('默认漫画源')
|
|
70
|
-
.default('jm'),
|
|
71
68
|
concurrency: koishi_1.Schema.number()
|
|
72
|
-
.
|
|
69
|
+
.min(1).max(16)
|
|
70
|
+
.description('下载并发数 (1-16)')
|
|
73
71
|
.default(4),
|
|
74
72
|
logInfo: koishi_1.Schema.boolean()
|
|
75
73
|
.description('打印 API 调用日志')
|
|
@@ -82,13 +80,6 @@ exports.Config = koishi_1.Schema.object({
|
|
|
82
80
|
fileSendPath: koishi_1.Schema.string()
|
|
83
81
|
.description('PDF 发送方式为 file 时的本地中转保存目录')
|
|
84
82
|
.default('/koishi/temp'),
|
|
85
|
-
tool: koishi_1.Schema.object({
|
|
86
|
-
enabled: koishi_1.Schema.boolean().default(true).description('开启后自动注册 ChatLuna 工具'),
|
|
87
|
-
name: koishi_1.Schema.string().default('comic').description('工具名称'),
|
|
88
|
-
description: koishi_1.Schema.string()
|
|
89
|
-
.default('漫画搜索与下载工具。支持禁漫天堂(JM)和哔咔漫画(Bika)双平台。包含动作:search (搜索), detail (查看详情), leaderboard (排行榜), latest (最近更新), random (随机推荐), download (下载本子并发送)。提示:如果用户给出的本子名、角色名或关键词模糊、包含缩写或拼写不够精确,或者使用该工具搜索未返回结果,你必须先调用联网搜索工具检索获取该本子的准确正式名称、画师/作者或完整标题,然后再用精准的关键词调用此工具。')
|
|
90
|
-
.description('工具描述'),
|
|
91
|
-
}).description('ChatLuna 工具设置'),
|
|
92
83
|
});
|
|
93
84
|
function getPdfPassword(source, comicId, chapterId) {
|
|
94
85
|
const src = source.trim().toLowerCase();
|
|
@@ -115,14 +106,6 @@ function determineSource(id) {
|
|
|
115
106
|
}
|
|
116
107
|
return null;
|
|
117
108
|
}
|
|
118
|
-
function formatComics(comics, sourceLabel = '') {
|
|
119
|
-
if (!comics || comics.length === 0)
|
|
120
|
-
return '没有找到相关漫画。';
|
|
121
|
-
return comics.map((c, i) => {
|
|
122
|
-
const src = c.source === 'jm' ? '禁漫天堂' : c.source === 'bika' ? '哔咔漫画' : sourceLabel;
|
|
123
|
-
return `${i + 1}. [${src}] ${c.title} 作者:${c.author || '佚名'}`;
|
|
124
|
-
}).join('\n');
|
|
125
|
-
}
|
|
126
109
|
function apply(ctx, config) {
|
|
127
110
|
const logger = ctx.logger('comic');
|
|
128
111
|
async function apiGet(path, params) {
|
|
@@ -134,9 +117,23 @@ function apply(ctx, config) {
|
|
|
134
117
|
}
|
|
135
118
|
if (config.logInfo)
|
|
136
119
|
logger.info(`GET ${url}`);
|
|
137
|
-
|
|
138
|
-
|
|
120
|
+
try {
|
|
121
|
+
const res = await ctx.http.get(url);
|
|
122
|
+
// 后端统一错误格式:{ success: false, error: '...' }
|
|
123
|
+
if (res && typeof res === 'object' && res.success === false) {
|
|
124
|
+
const msg = res.error || '后端返回未知错误';
|
|
125
|
+
logger.warn(`API 返回失败 [${url}]: ${msg}`);
|
|
126
|
+
throw new Error(msg);
|
|
127
|
+
}
|
|
128
|
+
return res;
|
|
129
|
+
}
|
|
130
|
+
catch (err) {
|
|
131
|
+
logger.error(`API 请求失败 [${url}]: ${err.message}`);
|
|
132
|
+
throw err;
|
|
133
|
+
}
|
|
139
134
|
}
|
|
135
|
+
// 子命令关键词集合,用于父命令路由时排除(避免被当作搜索词/下载ID)
|
|
136
|
+
const SUBCOMMANDS = ['search', 'download', 'detail', 'leaderboard', 'latest', 'random'];
|
|
140
137
|
// Parent command 'comic' routing logic
|
|
141
138
|
ctx.command('comic [keyword:text]', '聚合漫画搜索与直接下载')
|
|
142
139
|
.action(async ({ session }, keyword) => {
|
|
@@ -146,22 +143,31 @@ function apply(ctx, config) {
|
|
|
146
143
|
return session.execute('help comic');
|
|
147
144
|
}
|
|
148
145
|
const clean = keyword.trim();
|
|
146
|
+
// 关键修复:当 keyword 以子命令名开头时,强制用点号形式转发到子命令。
|
|
147
|
+
// Koishi 的 `comic [keyword:text]` 贪婪参数会吞掉 `comic random` / `comic latest`
|
|
148
|
+
// 这类无参子命令,导致它们被当作搜索词处理。
|
|
149
|
+
const parts = clean.split(/\s+/);
|
|
150
|
+
const firstWord = parts[0].toLowerCase();
|
|
151
|
+
if (SUBCOMMANDS.includes(firstWord)) {
|
|
152
|
+
const rest = parts.slice(1).join(' ');
|
|
153
|
+
return session.execute(`comic.${firstWord}${rest ? ' ' + rest : ''}`);
|
|
154
|
+
}
|
|
149
155
|
const resolved = determineSource(clean);
|
|
150
156
|
if (resolved) {
|
|
151
157
|
if (resolved.source === 'jm' && parseInt(resolved.id, 10) <= 100) {
|
|
152
|
-
return session.execute(`comic
|
|
158
|
+
return session.execute(`comic.search ${keyword}`);
|
|
153
159
|
}
|
|
154
|
-
return session.execute(`comic
|
|
160
|
+
return session.execute(`comic.download ${resolved.id}`);
|
|
155
161
|
}
|
|
156
162
|
// Fallback to search
|
|
157
|
-
return session.execute(`comic
|
|
163
|
+
return session.execute(`comic.search ${keyword}`);
|
|
158
164
|
});
|
|
159
|
-
|
|
165
|
+
// 构建单个漫画的详情文本;失败返回 null
|
|
166
|
+
async function buildDetailText(source, id) {
|
|
160
167
|
try {
|
|
161
|
-
await session.send('正在获取详情...');
|
|
162
168
|
const detail = await apiGet(`/api/comic/${source}/${id}`);
|
|
163
169
|
if (!detail?.title)
|
|
164
|
-
return
|
|
170
|
+
return null;
|
|
165
171
|
const srcLabel = source === 'jm' ? '禁漫天堂' : '哔咔漫画';
|
|
166
172
|
const parts = [
|
|
167
173
|
`📖 ${detail.title}`,
|
|
@@ -172,7 +178,7 @@ function apply(ctx, config) {
|
|
|
172
178
|
];
|
|
173
179
|
if (detail.chapters?.length > 0) {
|
|
174
180
|
const first = detail.chapters[0];
|
|
175
|
-
parts.push(
|
|
181
|
+
parts.push(`第一话: [${first.id}] ${first.name}`);
|
|
176
182
|
if (detail.chapters.length > 1) {
|
|
177
183
|
parts.push(`共 ${detail.chapters.length} 话,如需下载请使用 comic download <ID> [章节ID]`);
|
|
178
184
|
}
|
|
@@ -180,12 +186,17 @@ function apply(ctx, config) {
|
|
|
180
186
|
return parts.join('\n');
|
|
181
187
|
}
|
|
182
188
|
catch (err) {
|
|
183
|
-
logger.error(
|
|
184
|
-
return
|
|
189
|
+
logger.error(`获取详情失败 [${source}/${id}]:`, err.message);
|
|
190
|
+
return null;
|
|
185
191
|
}
|
|
186
192
|
}
|
|
193
|
+
async function fetchAndShowDetail(session, source, id) {
|
|
194
|
+
await session.send('正在获取详情...');
|
|
195
|
+
const text = await buildDetailText(source, id);
|
|
196
|
+
return text || '未找到该漫画';
|
|
197
|
+
}
|
|
187
198
|
// comic search <keyword>
|
|
188
|
-
ctx.command('comic
|
|
199
|
+
ctx.command('comic.search <keyword:text>', '聚合搜索漫画')
|
|
189
200
|
.action(async ({ session }, keyword) => {
|
|
190
201
|
if (!keyword)
|
|
191
202
|
return '请输入搜索关键词';
|
|
@@ -223,111 +234,76 @@ function apply(ctx, config) {
|
|
|
223
234
|
else {
|
|
224
235
|
result = await apiGet('/api/search', { keyword });
|
|
225
236
|
}
|
|
226
|
-
|
|
237
|
+
// 各平台最多取 8 条
|
|
238
|
+
const jmItems = (result.all_results?.jm || []).slice(0, 8).map(c => ({ ...c, source: 'jm' }));
|
|
239
|
+
const bikaItems = (result.all_results?.bika || []).slice(0, 8).map(c => ({ ...c, source: 'bika' }));
|
|
240
|
+
const totalResults = jmItems.length + bikaItems.length;
|
|
241
|
+
if (totalResults === 0) {
|
|
242
|
+
return `没有找到关于「${keyword}」的漫画。`;
|
|
243
|
+
}
|
|
244
|
+
// 统一编号:禁漫在前,哔咔在后,供回复序号下载
|
|
245
|
+
const all = [];
|
|
246
|
+
const jmLines = [];
|
|
247
|
+
const bikaLines = [];
|
|
248
|
+
let idx = 1;
|
|
249
|
+
for (const item of jmItems) {
|
|
250
|
+
all.push(item);
|
|
251
|
+
jmLines.push(`${idx}. [禁漫天堂] ${item.title} 作者:${item.author || '佚名'} (ID: ${item.id})`);
|
|
252
|
+
idx++;
|
|
253
|
+
}
|
|
254
|
+
for (const item of bikaItems) {
|
|
255
|
+
all.push(item);
|
|
256
|
+
bikaLines.push(`${idx}. [哔咔漫画] ${item.title} 作者:${item.author || '佚名'} (ID: ${item.id})`);
|
|
257
|
+
idx++;
|
|
258
|
+
}
|
|
259
|
+
if (!session) {
|
|
260
|
+
// 无会话环境(理论上不会发生),降级为纯文本
|
|
261
|
+
const lines = [];
|
|
262
|
+
if (jmLines.length)
|
|
263
|
+
lines.push('【 禁漫天堂 (JMComic) 】', ...jmLines);
|
|
264
|
+
if (bikaLines.length)
|
|
265
|
+
lines.push('【 哔咔漫画 (Bika) 】', ...bikaLines);
|
|
266
|
+
return lines.join('\n');
|
|
267
|
+
}
|
|
268
|
+
// 合并转发:禁漫一条、哔咔一条
|
|
269
|
+
const msgElements = [];
|
|
270
|
+
let header = `🔍 关键词「${keyword}」共找到 ${totalResults} 个结果`;
|
|
227
271
|
if (result.best_match?.title) {
|
|
228
272
|
const bm = result.best_match;
|
|
229
|
-
const
|
|
230
|
-
|
|
231
|
-
parts.push(` ${bm.title} 作者:${bm.author || '佚名'}`);
|
|
232
|
-
parts.push('');
|
|
273
|
+
const bmLabel = bm.source === 'jm' ? '禁漫天堂' : '哔咔漫画';
|
|
274
|
+
header += `\n🏆 最佳匹配 [${bmLabel}]: ${bm.title}`;
|
|
233
275
|
}
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
msgElements.push((0, koishi_1.h)('message', '【 哔咔漫画 (Bika) 】'));
|
|
257
|
-
for (const item of bikaItems) {
|
|
258
|
-
all.push(item);
|
|
259
|
-
msgElements.push((0, koishi_1.h)('message', ` ${currentIndex}. [哔咔漫画] ${item.title} 作者:${item.author || '佚名'}`));
|
|
260
|
-
currentIndex++;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
msgElements.push((0, koishi_1.h)('message', '💡 请查看上述列表后,在当前会话直接回复序号进行下载,回复其他内容退出。'));
|
|
264
|
-
await session.send((0, koishi_1.h)('message', { forward: true }, msgElements));
|
|
265
|
-
}
|
|
266
|
-
else {
|
|
267
|
-
parts.push(formatComics(jmItems, '禁漫天堂'));
|
|
268
|
-
parts.push('');
|
|
269
|
-
parts.push(formatComics(bikaItems, '哔咔漫画'));
|
|
270
|
-
return parts.join('\n');
|
|
271
|
-
}
|
|
276
|
+
msgElements.push((0, koishi_1.h)('message', header));
|
|
277
|
+
if (jmLines.length) {
|
|
278
|
+
msgElements.push((0, koishi_1.h)('message', `【 禁漫天堂 (JMComic) 】\n${jmLines.join('\n')}`));
|
|
279
|
+
}
|
|
280
|
+
if (bikaLines.length) {
|
|
281
|
+
msgElements.push((0, koishi_1.h)('message', `【 哔咔漫画 (Bika) 】\n${bikaLines.join('\n')}`));
|
|
282
|
+
}
|
|
283
|
+
msgElements.push((0, koishi_1.h)('message', '💡 回复序号下载(如 1),或回复「源|ID」(如 禁漫天堂|12345),回复其他内容退出。'));
|
|
284
|
+
await session.send((0, koishi_1.h)('message', { forward: true }, msgElements));
|
|
285
|
+
const answer = await session.prompt(30000);
|
|
286
|
+
if (!answer)
|
|
287
|
+
return;
|
|
288
|
+
const cleanAnswer = answer.trim();
|
|
289
|
+
let targetId = '';
|
|
290
|
+
const index = parseInt(cleanAnswer, 10);
|
|
291
|
+
if (!isNaN(index) && index > 0 && index <= all.length && /^\d+$/.test(cleanAnswer)) {
|
|
292
|
+
targetId = all[index - 1].id;
|
|
293
|
+
}
|
|
294
|
+
else {
|
|
295
|
+
const match = cleanAnswer.match(/^(?:禁漫天堂|哔咔漫画|禁漫|哔咔|jm|bika)[||](.+)$/i);
|
|
296
|
+
if (match) {
|
|
297
|
+
targetId = match[1].trim();
|
|
272
298
|
}
|
|
273
299
|
else {
|
|
274
|
-
|
|
275
|
-
let currentIndex = 1;
|
|
276
|
-
if (jmItems.length > 0) {
|
|
277
|
-
parts.push('【 禁漫天堂 (JMComic) 】');
|
|
278
|
-
for (const item of jmItems) {
|
|
279
|
-
all.push(item);
|
|
280
|
-
parts.push(` ${currentIndex}. [禁漫天堂] ${item.title} 作者:${item.author || '佚名'}`);
|
|
281
|
-
currentIndex++;
|
|
282
|
-
}
|
|
283
|
-
parts.push('');
|
|
284
|
-
}
|
|
285
|
-
if (bikaItems.length > 0) {
|
|
286
|
-
parts.push('【 哔咔漫画 (Bika) 】');
|
|
287
|
-
for (const item of bikaItems) {
|
|
288
|
-
all.push(item);
|
|
289
|
-
parts.push(` ${currentIndex}. [哔咔漫画] ${item.title} 作者:${item.author || '佚名'}`);
|
|
290
|
-
currentIndex++;
|
|
291
|
-
}
|
|
292
|
-
parts.push('');
|
|
293
|
-
}
|
|
294
|
-
parts.push('输入序号(例如:1)或「源|ID」(例如:禁漫天堂|12345)进行下载');
|
|
295
|
-
if (session) {
|
|
296
|
-
await session.send(parts.join('\n'));
|
|
297
|
-
}
|
|
298
|
-
else {
|
|
299
|
-
return parts.join('\n');
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
if (session) {
|
|
303
|
-
const answer = await session.prompt(30000);
|
|
304
|
-
if (!answer)
|
|
305
|
-
return;
|
|
306
|
-
const cleanAnswer = answer.trim();
|
|
307
|
-
let targetId = '';
|
|
308
|
-
const index = parseInt(cleanAnswer, 10);
|
|
309
|
-
if (!isNaN(index) && index > 0 && index <= all.length) {
|
|
310
|
-
const selected = all[index - 1];
|
|
311
|
-
targetId = selected.id;
|
|
312
|
-
}
|
|
313
|
-
else {
|
|
314
|
-
const match = cleanAnswer.match(/^(?:禁漫天堂|哔咔漫画|禁漫|哔咔|jm|bika)[||](.+)$/i);
|
|
315
|
-
if (match) {
|
|
316
|
-
targetId = match[1].trim();
|
|
317
|
-
}
|
|
318
|
-
else {
|
|
319
|
-
targetId = cleanAnswer;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
if (targetId) {
|
|
323
|
-
return session.execute(`comic download ${targetId}`);
|
|
324
|
-
}
|
|
325
|
-
return;
|
|
300
|
+
return; // 非有效输入,退出
|
|
326
301
|
}
|
|
327
302
|
}
|
|
328
|
-
|
|
329
|
-
return
|
|
303
|
+
if (targetId) {
|
|
304
|
+
return session.execute(`comic.download ${targetId}`);
|
|
330
305
|
}
|
|
306
|
+
return;
|
|
331
307
|
}
|
|
332
308
|
catch (err) {
|
|
333
309
|
logger.error('搜索失败:', err.message);
|
|
@@ -335,7 +311,7 @@ function apply(ctx, config) {
|
|
|
335
311
|
}
|
|
336
312
|
});
|
|
337
313
|
// comic download <id> [chapterId]
|
|
338
|
-
ctx.command('comic
|
|
314
|
+
ctx.command('comic.download <id:string> [chapterId:string]', '下载漫画PDF')
|
|
339
315
|
.action(async ({ session }, id, chapterId) => {
|
|
340
316
|
if (!id)
|
|
341
317
|
return '用法: comic download <ID> [章节ID]';
|
|
@@ -372,9 +348,17 @@ function apply(ctx, config) {
|
|
|
372
348
|
}
|
|
373
349
|
try {
|
|
374
350
|
await session.send('正在获取漫画详情...');
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
351
|
+
let detail;
|
|
352
|
+
try {
|
|
353
|
+
detail = await apiGet(`/api/comic/${source}/${targetId}`);
|
|
354
|
+
}
|
|
355
|
+
catch (e) {
|
|
356
|
+
return `获取详情失败(后端请求出错):${e.message}\n可能原因:comic-api 未启动、源站反爬拦截或 ID 不存在。`;
|
|
357
|
+
}
|
|
358
|
+
if (!detail?.title) {
|
|
359
|
+
const srcLabel = source === 'jm' ? '禁漫天堂' : '哔咔漫画';
|
|
360
|
+
return `未找到该漫画 [${srcLabel} ID: ${targetId}]\n请确认 ID 是否正确;若是哔咔需先登录,禁漫可能被源站临时拦截。`;
|
|
361
|
+
}
|
|
378
362
|
if (!detail.chapters?.length)
|
|
379
363
|
return '该漫画没有可下载的章节';
|
|
380
364
|
const chapter = chapterId
|
|
@@ -433,141 +417,125 @@ function apply(ctx, config) {
|
|
|
433
417
|
}
|
|
434
418
|
});
|
|
435
419
|
// comic detail <id>
|
|
436
|
-
ctx.command('comic
|
|
420
|
+
ctx.command('comic.detail <id:text>', '查看漫画详情')
|
|
437
421
|
.action(async ({ session }, query) => {
|
|
438
422
|
if (!query)
|
|
439
423
|
return '用法: comic detail <关键词> 或 comic detail <ID>';
|
|
440
424
|
if (!session)
|
|
441
425
|
return '此命令仅支持在会话中使用';
|
|
426
|
+
// 直接给定 ID/源:只查该来源
|
|
442
427
|
const resolved = determineSource(query);
|
|
443
428
|
if (resolved) {
|
|
444
429
|
return fetchAndShowDetail(session, resolved.source, resolved.id);
|
|
445
430
|
}
|
|
431
|
+
// 关键词模式:禁漫和哔咔各取 best 匹配,分别展示详情
|
|
446
432
|
try {
|
|
433
|
+
await session.send('正在搜索并获取详情...');
|
|
447
434
|
const result = await apiGet('/api/search', { keyword: query });
|
|
448
|
-
const
|
|
449
|
-
const
|
|
450
|
-
|
|
451
|
-
...jmList.map(c => ({ ...c, source: 'jm' })),
|
|
452
|
-
...bikaList.map(c => ({ ...c, source: 'bika' })),
|
|
453
|
-
];
|
|
454
|
-
if (all.length === 0) {
|
|
435
|
+
const jmBest = (result.all_results?.jm || [])[0];
|
|
436
|
+
const bikaBest = (result.all_results?.bika || [])[0];
|
|
437
|
+
if (!jmBest && !bikaBest) {
|
|
455
438
|
return `没有找到关于「${query}」的漫画。`;
|
|
456
439
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
if (all.length > 5) {
|
|
469
|
-
parts.push(`找到 ${all.length} 个结果,已生成合并转发记录:`);
|
|
470
|
-
await session.send(parts.join('\n'));
|
|
471
|
-
const msgElements = all.map((c, i) => {
|
|
472
|
-
const src = c.source === 'jm' ? '禁漫天堂' : '哔咔漫画';
|
|
473
|
-
return (0, koishi_1.h)('message', `${i + 1}. [${src}] ${c.title} 作者:${c.author || '佚名'}`);
|
|
474
|
-
});
|
|
475
|
-
msgElements.push((0, koishi_1.h)('message', `💡 请回复序号(1-${all.length})查看详情,或回复其他任意内容退出。`));
|
|
476
|
-
await session.send((0, koishi_1.h)('message', { forward: true }, msgElements));
|
|
477
|
-
}
|
|
478
|
-
else {
|
|
479
|
-
parts.push(`找到以下多个结果,请回复序号(1-${all.length})查看详情,或回复其他任意内容退出:`);
|
|
480
|
-
parts.push(formatComics(all));
|
|
481
|
-
await session.send(parts.join('\n'));
|
|
482
|
-
}
|
|
483
|
-
const answer = await session.prompt(30000);
|
|
484
|
-
if (!answer)
|
|
485
|
-
return;
|
|
486
|
-
const trimmed = answer.trim();
|
|
487
|
-
const index = parseInt(trimmed, 10);
|
|
488
|
-
if (!isNaN(index) && index >= 1 && index <= all.length) {
|
|
489
|
-
const chosen = all[index - 1];
|
|
490
|
-
return fetchAndShowDetail(session, chosen.source, chosen.id);
|
|
491
|
-
}
|
|
492
|
-
else {
|
|
493
|
-
return '输入无效,已退出。';
|
|
494
|
-
}
|
|
440
|
+
const [jmText, bikaText] = await Promise.all([
|
|
441
|
+
jmBest ? buildDetailText('jm', jmBest.id) : Promise.resolve(null),
|
|
442
|
+
bikaBest ? buildDetailText('bika', bikaBest.id) : Promise.resolve(null),
|
|
443
|
+
]);
|
|
444
|
+
const msgElements = [
|
|
445
|
+
(0, koishi_1.h)('message', `🔍 关键词「${query}」各平台最相似结果详情:`),
|
|
446
|
+
];
|
|
447
|
+
msgElements.push((0, koishi_1.h)('message', jmText || '【 禁漫天堂 】无匹配结果'));
|
|
448
|
+
msgElements.push((0, koishi_1.h)('message', bikaText || '【 哔咔漫画 】无匹配结果(或需先登录)'));
|
|
449
|
+
await session.send((0, koishi_1.h)('message', { forward: true }, msgElements));
|
|
450
|
+
return;
|
|
495
451
|
}
|
|
496
452
|
catch (err) {
|
|
497
453
|
logger.error('详情搜索失败:', err.message);
|
|
498
454
|
return `查询失败: ${err.message}`;
|
|
499
455
|
}
|
|
500
456
|
});
|
|
501
|
-
// comic leaderboard [mode]
|
|
502
|
-
ctx.command('comic
|
|
503
|
-
.action(async ({ session }, mode) => {
|
|
457
|
+
// comic leaderboard [mode] [page]
|
|
458
|
+
ctx.command('comic.leaderboard [mode:string] [page:number]', '查看排行榜')
|
|
459
|
+
.action(async ({ session }, mode, page) => {
|
|
460
|
+
if (!session)
|
|
461
|
+
return '此命令仅支持在会话中使用';
|
|
462
|
+
// 未指定类型默认日榜(最新的当日榜单)
|
|
504
463
|
const targetMode = (mode || 'day').toLowerCase();
|
|
505
464
|
if (!['day', 'week', 'month', 'total'].includes(targetMode)) {
|
|
506
465
|
return 'mode 必须是 day/week/month/total';
|
|
507
466
|
}
|
|
467
|
+
const targetPage = Math.max(1, Math.floor(page || 1));
|
|
468
|
+
const query = { mode: targetMode, page: String(targetPage) };
|
|
508
469
|
try {
|
|
509
470
|
const [jmResult, bikaResult] = await Promise.all([
|
|
510
|
-
apiGet('/api/jm/leaderboard',
|
|
511
|
-
apiGet('/api/bika/leaderboard',
|
|
471
|
+
apiGet('/api/jm/leaderboard', query).catch(() => null),
|
|
472
|
+
apiGet('/api/bika/leaderboard', query).catch(() => null),
|
|
512
473
|
]);
|
|
513
474
|
const modeMap = { day: '日榜', week: '周榜', month: '月榜', total: '总榜' };
|
|
514
|
-
const
|
|
515
|
-
const formatLeaderboard = (comics) => {
|
|
475
|
+
const formatList = (comics) => {
|
|
516
476
|
if (!comics || comics.length === 0)
|
|
517
477
|
return '暂无数据或获取失败';
|
|
518
|
-
return comics.map((c, i) => {
|
|
519
|
-
return `${i + 1}. ${c.title} 作者:${c.author || '佚名'} (ID: ${c.id})`;
|
|
520
|
-
}).join('\n');
|
|
478
|
+
return comics.map((c, i) => `${i + 1}. ${c.title} 作者:${c.author || '佚名'} (ID: ${c.id})`).join('\n');
|
|
521
479
|
};
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
parts.push(formatLeaderboard(bikaResult.data));
|
|
533
|
-
}
|
|
534
|
-
else {
|
|
535
|
-
parts.push('暂无数据或获取失败');
|
|
536
|
-
}
|
|
537
|
-
return parts.join('\n');
|
|
480
|
+
const jmOk = jmResult && jmResult.success !== false && jmResult.data?.length;
|
|
481
|
+
const bikaOk = bikaResult && bikaResult.success !== false && bikaResult.data?.length;
|
|
482
|
+
const jmText = `【 禁漫天堂 (JMComic) ${modeMap[targetMode]} · 第${targetPage}页 】\n` + (jmOk ? formatList(jmResult.data) : '暂无数据或获取失败');
|
|
483
|
+
// 哔咔无总榜,自动回退日榜
|
|
484
|
+
const bikaModeLabel = targetMode === 'total' ? '日榜(哔咔无总榜)' : modeMap[targetMode];
|
|
485
|
+
const bikaText = `【 哔咔漫画 (Bika) ${bikaModeLabel} · 第${targetPage}页 】\n` + (bikaOk ? formatList(bikaResult.data) : '暂无数据或获取失败(哔咔需先登录)');
|
|
486
|
+
// 禁漫、哔咔分成 2 条独立消息发送
|
|
487
|
+
await session.send(jmText);
|
|
488
|
+
await session.send(bikaText);
|
|
489
|
+
return;
|
|
538
490
|
}
|
|
539
491
|
catch (err) {
|
|
540
492
|
logger.error('获取排行榜失败:', err.message);
|
|
541
493
|
return `获取排行榜失败: ${err.message}`;
|
|
542
494
|
}
|
|
543
495
|
});
|
|
496
|
+
// 翻页累积拉取,直到达到 limit 条或没有更多(最多翻 maxPages 页防止狂刷后端)
|
|
497
|
+
async function fetchUpTo(source, endpoint, limit, maxPages = 5) {
|
|
498
|
+
const acc = [];
|
|
499
|
+
for (let page = 1; page <= maxPages && acc.length < limit; page++) {
|
|
500
|
+
let res = null;
|
|
501
|
+
try {
|
|
502
|
+
res = await apiGet(`/api/${source}/${endpoint}`, { page: String(page) });
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
break;
|
|
506
|
+
}
|
|
507
|
+
if (!res || res.success === false || !res.data?.length)
|
|
508
|
+
break;
|
|
509
|
+
acc.push(...res.data);
|
|
510
|
+
if (res.data.length === 0)
|
|
511
|
+
break;
|
|
512
|
+
}
|
|
513
|
+
return acc.slice(0, limit);
|
|
514
|
+
}
|
|
544
515
|
// comic latest
|
|
545
|
-
ctx.command('comic
|
|
516
|
+
ctx.command('comic.latest', '查看最近更新')
|
|
546
517
|
.alias('最新漫画')
|
|
547
518
|
.alias('漫画更新')
|
|
548
|
-
.
|
|
519
|
+
.option('number', '-n <count:number> 每个平台显示数量(默认10,最多50)')
|
|
520
|
+
.action(async ({ session, options }) => {
|
|
549
521
|
if (!session)
|
|
550
522
|
return '此命令仅支持在会话中使用';
|
|
523
|
+
const limit = Math.min(50, Math.max(1, Math.floor(options?.number || 10)));
|
|
551
524
|
try {
|
|
552
|
-
const [
|
|
553
|
-
|
|
554
|
-
|
|
525
|
+
const [jmItems, bikaItems] = await Promise.all([
|
|
526
|
+
fetchUpTo('jm', 'latest', limit).catch(() => []),
|
|
527
|
+
fetchUpTo('bika', 'latest', limit).catch(() => []),
|
|
555
528
|
]);
|
|
556
|
-
const
|
|
529
|
+
const formatList = (comics) => {
|
|
557
530
|
if (!comics || comics.length === 0)
|
|
558
531
|
return '暂无数据或获取失败';
|
|
559
|
-
|
|
560
|
-
return list.map((c, i) => {
|
|
561
|
-
return `${i + 1}. ${c.title} 作者:${c.author || '佚名'} (ID: ${c.id})`;
|
|
562
|
-
}).join('\n');
|
|
532
|
+
return comics.map((c, i) => `${i + 1}. ${c.title} 作者:${c.author || '佚名'} (ID: ${c.id})`).join('\n');
|
|
563
533
|
};
|
|
564
|
-
const jmText = `【 禁漫天堂 (JMComic) 最近更新 】\n` + (
|
|
565
|
-
const bikaText = `【 哔咔漫画 (Bika) 最近更新 】\n` + (
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
];
|
|
570
|
-
await session.send((0, koishi_1.h)('message', { forward: true }, msgElements));
|
|
534
|
+
const jmText = `【 禁漫天堂 (JMComic) 最近更新 · ${jmItems.length}条 】\n` + formatList(jmItems);
|
|
535
|
+
const bikaText = `【 哔咔漫画 (Bika) 最近更新 · ${bikaItems.length}条 】\n` + (bikaItems.length ? formatList(bikaItems) : '暂无数据或获取失败(哔咔需先登录)');
|
|
536
|
+
// 禁漫、哔咔分成 2 条独立消息发送
|
|
537
|
+
await session.send(jmText);
|
|
538
|
+
await session.send(bikaText);
|
|
571
539
|
return;
|
|
572
540
|
}
|
|
573
541
|
catch (err) {
|
|
@@ -575,182 +543,213 @@ function apply(ctx, config) {
|
|
|
575
543
|
return `获取最近更新失败: ${err.message}`;
|
|
576
544
|
}
|
|
577
545
|
});
|
|
578
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
546
|
+
// 随机推荐:多次调用累积去重,直到达到 limit 条或尝试上限
|
|
547
|
+
async function fetchRandomUpTo(source, limit, maxTries = 4) {
|
|
548
|
+
const map = new Map();
|
|
549
|
+
for (let i = 0; i < maxTries && map.size < limit; i++) {
|
|
550
|
+
let res = null;
|
|
551
|
+
try {
|
|
552
|
+
res = await apiGet(`/api/${source}/random`);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
if (!res || res.success === false || !res.data?.length)
|
|
558
|
+
break;
|
|
559
|
+
for (const item of res.data) {
|
|
560
|
+
if (item?.id && !map.has(item.id))
|
|
561
|
+
map.set(item.id, item);
|
|
591
562
|
}
|
|
592
|
-
return `${srcLabel} 随机推荐暂无数据`;
|
|
593
563
|
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
564
|
+
return Array.from(map.values()).slice(0, limit);
|
|
565
|
+
}
|
|
566
|
+
// comic random
|
|
567
|
+
ctx.command('comic.random', '随机推荐漫画')
|
|
568
|
+
.alias('随机漫画')
|
|
569
|
+
.option('number', '-n <count:number> 每个平台推荐数量(默认5,最多20)')
|
|
570
|
+
.action(async ({ session, options }) => {
|
|
571
|
+
const limit = Math.min(20, Math.max(1, Math.floor(options?.number || 5)));
|
|
572
|
+
const [jmItems, bikaItems] = await Promise.all([
|
|
573
|
+
fetchRandomUpTo('jm', limit).catch(() => []),
|
|
574
|
+
fetchRandomUpTo('bika', limit).catch(() => []),
|
|
575
|
+
]);
|
|
576
|
+
const formatList = (comics, label) => {
|
|
577
|
+
if (!comics || comics.length === 0)
|
|
578
|
+
return `【 ${label} 】暂无数据或获取失败`;
|
|
579
|
+
const lines = comics.map((c, i) => `${i + 1}. [${label}] ${c.title} 作者:${c.author || '佚名'} (ID: ${c.id})`);
|
|
580
|
+
return `🎲 ${label} 随机推荐 · ${comics.length}个\n${lines.join('\n')}`;
|
|
581
|
+
};
|
|
582
|
+
const jmText = formatList(jmItems, '禁漫天堂');
|
|
583
|
+
const bikaText = bikaItems.length ? formatList(bikaItems, '哔咔漫画') : '🎲 哔咔漫画 随机推荐\n暂无数据或获取失败(哔咔需先登录)';
|
|
584
|
+
if (session) {
|
|
585
|
+
// 禁漫、哔咔分成 2 条独立消息发送
|
|
586
|
+
await session.send(jmText);
|
|
587
|
+
await session.send(bikaText);
|
|
588
|
+
await session.send('💡 如需下载,请使用 comic download <ID>');
|
|
589
|
+
return;
|
|
598
590
|
}
|
|
591
|
+
return [jmText, '', bikaText].join('\n');
|
|
599
592
|
});
|
|
600
593
|
// ========== ChatLuna 工具注册 ==========
|
|
601
594
|
ctx.on('ready', async () => {
|
|
602
|
-
if (!config.tool.enabled)
|
|
603
|
-
return;
|
|
604
595
|
if (!ctx.chatluna) {
|
|
605
596
|
logger.info('chatluna 未安装,跳过注册 ChatLuna 工具');
|
|
606
597
|
return;
|
|
607
598
|
}
|
|
608
|
-
const
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
createTool() {
|
|
615
|
-
return createComicTool(ctx, config);
|
|
616
|
-
},
|
|
617
|
-
meta: {
|
|
618
|
-
source: 'extension',
|
|
619
|
-
group: 'comic',
|
|
620
|
-
tags: ['comic', 'manga', 'jmcomic', 'bika'],
|
|
621
|
-
defaultAvailability: {
|
|
622
|
-
enabled: true,
|
|
623
|
-
main: true,
|
|
624
|
-
chatluna: true,
|
|
625
|
-
characterScope: 'all',
|
|
599
|
+
const chatluna = ctx.chatluna;
|
|
600
|
+
const registerSingleTool = (name, description, schema, run) => {
|
|
601
|
+
chatluna.platform.registerTool(name, {
|
|
602
|
+
description,
|
|
603
|
+
selector() {
|
|
604
|
+
return true;
|
|
626
605
|
},
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
mode: zod_1.z.enum(['day', 'week', 'month', 'total']).optional()
|
|
644
|
-
.describe('排行榜时间维度(action=leaderboard 时使用):day=日榜, week=周榜, month=月榜, total=总榜'),
|
|
645
|
-
page: zod_1.z.number().int().min(1).optional()
|
|
646
|
-
.describe('页码,默认1'),
|
|
647
|
-
});
|
|
648
|
-
function createComicTool(ctx, cfg) {
|
|
649
|
-
const description = (cfg.tool.description || '').trim() || '漫画搜索与下载工具。支持禁漫天堂(JM)和哔咔漫画(Bika)双平台。包含动作:search (搜索), detail (查看详情), leaderboard (排行榜), latest (最近更新), random (随机推荐), download (下载本子并发送)。提示:如果用户给出的本子名、角色名或关键词模糊、包含缩写或拼写不够精确,或者使用该工具搜索未返回结果,你必须先调用联网搜索工具检索获取该本子的准确正式名称、画师/作者或完整标题,然后再用精准的关键词调用此工具。';
|
|
650
|
-
const name = (cfg.tool.name || 'comic').trim() || 'comic';
|
|
651
|
-
return (0, tools_1.tool)(async (input, runConfig) => {
|
|
652
|
-
const logger = ctx.logger('comic');
|
|
653
|
-
const source = input.source || cfg.defaultSource || 'jm';
|
|
654
|
-
const apiBase = cfg.apiBase.replace(/\/+$/, '');
|
|
655
|
-
const apiGet = async (path, params) => {
|
|
656
|
-
let url = apiBase + path;
|
|
657
|
-
if (params) {
|
|
658
|
-
const qs = new URLSearchParams(params).toString();
|
|
659
|
-
if (qs)
|
|
660
|
-
url += '?' + qs;
|
|
661
|
-
}
|
|
662
|
-
return ctx.http.get(url);
|
|
606
|
+
createTool() {
|
|
607
|
+
return (0, tools_1.tool)(run, { name, description, schema });
|
|
608
|
+
},
|
|
609
|
+
meta: {
|
|
610
|
+
source: 'extension',
|
|
611
|
+
group: 'comic',
|
|
612
|
+
tags: ['comic', 'manga', 'jmcomic', 'bika'],
|
|
613
|
+
defaultAvailability: {
|
|
614
|
+
enabled: true,
|
|
615
|
+
main: true,
|
|
616
|
+
chatluna: true,
|
|
617
|
+
characterScope: 'all',
|
|
618
|
+
},
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
logger.info(`ChatLuna 工具「${name}」已注册`);
|
|
663
622
|
};
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
}
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
return JSON.stringify({ error: '查看详情时 comic_id 不能为空' });
|
|
699
|
-
}
|
|
700
|
-
const detail = await apiGet(`/api/comic/${source}/${input.comic_id}`);
|
|
701
|
-
if (!detail?.title) {
|
|
702
|
-
return JSON.stringify({ error: '未找到该漫画' });
|
|
703
|
-
}
|
|
704
|
-
return JSON.stringify({
|
|
705
|
-
title: detail.title,
|
|
706
|
-
author: detail.author,
|
|
707
|
-
description: detail.description,
|
|
708
|
-
source: detail.source || source,
|
|
709
|
-
chapter_count: detail.chapters?.length || 0,
|
|
710
|
-
chapters: detail.chapters?.slice(0, 30) || [],
|
|
711
|
-
});
|
|
712
|
-
}
|
|
713
|
-
case 'leaderboard': {
|
|
714
|
-
const mode = input.mode || 'day';
|
|
715
|
-
const page = String(input.page || 1);
|
|
716
|
-
const result = await apiGet(`/api/${source}/leaderboard`, { mode, page });
|
|
717
|
-
return JSON.stringify({
|
|
718
|
-
source,
|
|
719
|
-
mode,
|
|
720
|
-
page: Number(page),
|
|
721
|
-
total: result.data?.length || 0,
|
|
722
|
-
results: result.data || [],
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
case 'latest': {
|
|
726
|
-
const page = String(input.page || 1);
|
|
727
|
-
const result = await apiGet(`/api/${source}/latest`, { page });
|
|
728
|
-
return JSON.stringify({
|
|
729
|
-
source,
|
|
730
|
-
page: Number(page),
|
|
731
|
-
total: result.data?.length || 0,
|
|
732
|
-
results: result.data || [],
|
|
733
|
-
});
|
|
623
|
+
// 1. comic_search
|
|
624
|
+
registerSingleTool('comic_search', '搜索漫画,支持禁漫天堂(JM)和哔咔漫画(Bika)双平台聚合搜索。提示:如果用户给出的本子名、角色名或关键词模糊,或搜索未返回结果,必须先调用联网搜索工具检索获取该本子的准确正式名称或完整标题,然后再用精准的关键词调用此工具。', zod_1.z.object({
|
|
625
|
+
keyword: zod_1.z.string().describe('搜索关键词'),
|
|
626
|
+
}), async (input) => {
|
|
627
|
+
try {
|
|
628
|
+
const result = await apiGet('/api/search', { keyword: input.keyword });
|
|
629
|
+
const jmList = result.all_results?.jm || [];
|
|
630
|
+
const bikaList = result.all_results?.bika || [];
|
|
631
|
+
const all = [
|
|
632
|
+
...jmList.map(c => ({ ...c, source: 'jm' })),
|
|
633
|
+
...bikaList.map(c => ({ ...c, source: 'bika' })),
|
|
634
|
+
];
|
|
635
|
+
return JSON.stringify({
|
|
636
|
+
keyword: input.keyword,
|
|
637
|
+
best_match: result.best_match,
|
|
638
|
+
total: all.length,
|
|
639
|
+
results: all.slice(0, 20),
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
catch (err) {
|
|
643
|
+
logger.error('工具 comic_search 调用失败:', err.message);
|
|
644
|
+
return JSON.stringify({ error: err.message || '请求失败' });
|
|
645
|
+
}
|
|
646
|
+
});
|
|
647
|
+
// 2. comic_detail
|
|
648
|
+
registerSingleTool('comic_detail', '查看指定来源和ID的漫画详情,包括标题、作者、简介以及可下载的章节列表。', zod_1.z.object({
|
|
649
|
+
comic_id: zod_1.z.string().describe('漫画ID'),
|
|
650
|
+
source: zod_1.z.enum(['jm', 'bika']).optional().default('jm').describe('漫画源:jm=禁漫天堂, bika=哔咔漫画。默认jm。'),
|
|
651
|
+
}), async (input) => {
|
|
652
|
+
try {
|
|
653
|
+
const source = input.source || 'jm';
|
|
654
|
+
const detail = await apiGet(`/api/comic/${source}/${input.comic_id}`);
|
|
655
|
+
if (!detail?.title) {
|
|
656
|
+
return JSON.stringify({ error: '未找到该漫画' });
|
|
734
657
|
}
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
658
|
+
return JSON.stringify({
|
|
659
|
+
title: detail.title,
|
|
660
|
+
author: detail.author,
|
|
661
|
+
description: detail.description,
|
|
662
|
+
source: detail.source || source,
|
|
663
|
+
chapter_count: detail.chapters?.length || 0,
|
|
664
|
+
chapters: detail.chapters?.slice(0, 30) || [],
|
|
665
|
+
});
|
|
666
|
+
}
|
|
667
|
+
catch (err) {
|
|
668
|
+
logger.error('工具 comic_detail 调用失败:', err.message);
|
|
669
|
+
return JSON.stringify({ error: err.message || '请求失败' });
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
// 3. comic_leaderboard
|
|
673
|
+
registerSingleTool('comic_leaderboard', '查看指定漫画源的排行榜。', zod_1.z.object({
|
|
674
|
+
source: zod_1.z.enum(['jm', 'bika']).optional().default('jm').describe('漫画源:jm=禁漫天堂, bika=哔咔漫画。默认jm。'),
|
|
675
|
+
mode: zod_1.z.enum(['day', 'week', 'month', 'total']).optional().default('day').describe('排行榜时间维度:day=日榜, week=周榜, month=月榜, total=总榜。默认day。'),
|
|
676
|
+
page: zod_1.z.number().int().min(1).optional().default(1).describe('页码,默认1。'),
|
|
677
|
+
}), async (input) => {
|
|
678
|
+
try {
|
|
679
|
+
const source = input.source || 'jm';
|
|
680
|
+
const mode = input.mode || 'day';
|
|
681
|
+
const page = String(input.page || 1);
|
|
682
|
+
const result = await apiGet(`/api/${source}/leaderboard`, { mode, page });
|
|
683
|
+
return JSON.stringify({
|
|
684
|
+
source,
|
|
685
|
+
mode,
|
|
686
|
+
page: Number(page),
|
|
687
|
+
total: result.data?.length || 0,
|
|
688
|
+
results: result.data || [],
|
|
689
|
+
});
|
|
690
|
+
}
|
|
691
|
+
catch (err) {
|
|
692
|
+
logger.error('工具 comic_leaderboard 调用失败:', err.message);
|
|
693
|
+
return JSON.stringify({ error: err.message || '请求失败' });
|
|
694
|
+
}
|
|
695
|
+
});
|
|
696
|
+
// 4. comic_latest
|
|
697
|
+
registerSingleTool('comic_latest', '查看指定漫画源的最近更新列表。', zod_1.z.object({
|
|
698
|
+
source: zod_1.z.enum(['jm', 'bika']).optional().default('jm').describe('漫画源:jm=禁漫天堂, bika=哔咔漫画。默认jm。'),
|
|
699
|
+
page: zod_1.z.number().int().min(1).optional().default(1).describe('页码,默认1。'),
|
|
700
|
+
}), async (input) => {
|
|
701
|
+
try {
|
|
702
|
+
const source = input.source || 'jm';
|
|
703
|
+
const page = String(input.page || 1);
|
|
704
|
+
const result = await apiGet(`/api/${source}/latest`, { page });
|
|
705
|
+
return JSON.stringify({
|
|
706
|
+
source,
|
|
707
|
+
page: Number(page),
|
|
708
|
+
total: result.data?.length || 0,
|
|
709
|
+
results: result.data || [],
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
catch (err) {
|
|
713
|
+
logger.error('工具 comic_latest 调用失败:', err.message);
|
|
714
|
+
return JSON.stringify({ error: err.message || '请求失败' });
|
|
715
|
+
}
|
|
716
|
+
});
|
|
717
|
+
// 5. comic_random
|
|
718
|
+
registerSingleTool('comic_random', '随机推荐指定漫画源的漫画。', zod_1.z.object({
|
|
719
|
+
source: zod_1.z.enum(['jm', 'bika']).optional().default('jm').describe('漫画源:jm=禁漫天堂, bika=哔咔漫画。默认jm。'),
|
|
720
|
+
}), async (input) => {
|
|
721
|
+
try {
|
|
722
|
+
const source = input.source || 'jm';
|
|
723
|
+
const result = await apiGet(`/api/${source}/random`);
|
|
724
|
+
return JSON.stringify({
|
|
725
|
+
source,
|
|
726
|
+
result: result.data || null,
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
catch (err) {
|
|
730
|
+
logger.error('工具 comic_random 调用失败:', err.message);
|
|
731
|
+
return JSON.stringify({ error: err.message || '请求失败' });
|
|
732
|
+
}
|
|
733
|
+
});
|
|
734
|
+
// 6. comic_download
|
|
735
|
+
registerSingleTool('comic_download', '下载指定ID的漫画并发送给用户。会自动启动后台下载并向用户发送PDF文件及解密密码。', zod_1.z.object({
|
|
736
|
+
comic_id: zod_1.z.string().describe('漫画ID'),
|
|
737
|
+
chapter_id: zod_1.z.string().optional().describe('章节ID(选填,不填默认下载第一话)'),
|
|
738
|
+
}), async (input, runConfig) => {
|
|
739
|
+
try {
|
|
740
|
+
const session = runConfig?.configurable?.session;
|
|
741
|
+
if (!session) {
|
|
742
|
+
return JSON.stringify({ error: '无法获取当前会话,不支持下载操作。请让用户在聊天界面直接使用 comic 命令或 comic.download 命令手动下载。' });
|
|
741
743
|
}
|
|
742
|
-
|
|
743
|
-
|
|
744
|
+
const cmd = input.chapter_id ? `comic.download ${input.comic_id} ${input.chapter_id}` : `comic.download ${input.comic_id}`;
|
|
745
|
+
session.execute(cmd);
|
|
746
|
+
return JSON.stringify({ success: true, message: `已在后台启动漫画下载,命令为: ${cmd}。请告知用户正在下载,并让其留意后续接收的 PDF 文件与密码。` });
|
|
744
747
|
}
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
}
|
|
750
|
-
}, {
|
|
751
|
-
name,
|
|
752
|
-
description,
|
|
753
|
-
schema: comicToolSchema,
|
|
748
|
+
catch (err) {
|
|
749
|
+
logger.error('工具 comic_download 调用失败:', err.message);
|
|
750
|
+
return JSON.stringify({ error: err.message || '请求失败' });
|
|
751
|
+
}
|
|
752
|
+
});
|
|
754
753
|
});
|
|
755
754
|
}
|
|
756
755
|
//# sourceMappingURL=index.js.map
|