koishi-plugin-comic 1.0.11 → 1.1.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/lib/index.d.ts +41 -3
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +172 -194
- package/lib/index.js.map +1 -1
- package/package.json +6 -12
- package/lib/comic-api.d.ts +0 -70
- package/lib/comic-api.d.ts.map +0 -1
- package/lib/comic-api.js +0 -110
- package/lib/comic-api.js.map +0 -1
- package/lib/tools.d.ts +0 -112
- package/lib/tools.d.ts.map +0 -1
- package/lib/tools.js +0 -317
- package/lib/tools.js.map +0 -1
package/lib/index.d.ts
CHANGED
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import { Context, Schema } from 'koishi';
|
|
2
2
|
declare module 'koishi' {
|
|
3
3
|
interface Context {
|
|
4
|
-
chatluna
|
|
4
|
+
chatluna: {
|
|
5
5
|
platform: {
|
|
6
|
-
registerTool(name: string, options:
|
|
6
|
+
registerTool(name: string, options: {
|
|
7
|
+
description: string;
|
|
8
|
+
selector: () => boolean;
|
|
9
|
+
createTool: () => any;
|
|
10
|
+
meta: {
|
|
11
|
+
source: string;
|
|
12
|
+
group: string;
|
|
13
|
+
tags: string[];
|
|
14
|
+
defaultAvailability: {
|
|
15
|
+
enabled: boolean;
|
|
16
|
+
main: boolean;
|
|
17
|
+
chatluna: boolean;
|
|
18
|
+
characterScope: string;
|
|
19
|
+
};
|
|
20
|
+
};
|
|
21
|
+
}): () => void;
|
|
7
22
|
};
|
|
23
|
+
createChatModel(target: string): Promise<any>;
|
|
8
24
|
};
|
|
9
25
|
}
|
|
10
26
|
}
|
|
11
27
|
export declare const inject: {
|
|
12
28
|
required: string[];
|
|
13
|
-
optional: string[];
|
|
14
29
|
};
|
|
15
30
|
export declare const usage = "\n<h2>\u805A\u5408\u6F2B\u753B\u641C\u7D22\u4E0E\u4E0B\u8F7D\u63D2\u4EF6</h2>\n<p>\u4F7F\u7528\u524D\u8BF7\u5148\u90E8\u7F72 <a href=\"https://github.com/lumia1998/comic-api\">comic-api</a> \u540E\u7AEF\u670D\u52A1</p>\n<p>\u652F\u6301\u7981\u6F2B\u5929\u5802(JM)\u548C\u54D4\u5494\u6F2B\u753B(Bika)\u53CC\u5E73\u53F0\u805A\u5408\u641C\u7D22</p>\n<h3>\u547D\u4EE4\u5217\u8868</h3>\n<ul>\n <li><code>comic [\u5173\u952E\u8BCD]</code> - \u805A\u5408\u641C\u7D22/\u76F4\u63A5\u4E0B\u8F7D\u6F2B\u753B</li>\n <li><code>comic search <\u5173\u952E\u8BCD/ID></code> - \u805A\u5408\u641C\u7D22\u6F2B\u753B\uFF0C\u5404\u5E73\u53F0\u6700\u591A8\u6761\uFF0C\u5408\u5E76\u8F6C\u53D1\u5E76\u6807\u6CE8\u6765\u6E90</li>\n <li><code>comic download <ID> [\u7AE0\u8282ID]</code> - \u4E0B\u8F7D\u6F2B\u753BPDF</li>\n <li><code>comic detail <\u5173\u952E\u8BCD/ID></code> - \u67E5\u770B\u6F2B\u753B\u8BE6\u60C5\uFF0C\u5173\u952E\u8BCD\u6A21\u5F0F\u4E0B\u53CC\u5E73\u53F0\u5404\u5C55\u793A\u6700\u76F8\u4F3C\u7ED3\u679C</li>\n <li><code>comic leaderboard [\u7C7B\u578B] [\u9875\u7801]</code> - \u6392\u884C\u699C(\u7C7B\u578B: day/week/month/total\uFF0C\u9ED8\u8BA4day)\uFF0C\u53CC\u5E73\u53F0\u52062\u6761\u53D1\u9001</li>\n <li><code>comic latest [-n \u6570\u91CF]</code> - \u6700\u8FD1\u66F4\u65B0\uFF0C\u6BCF\u5E73\u53F0\u9ED8\u8BA410\u6761(\u6700\u591A50)\uFF0C\u53CC\u5E73\u53F0\u52062\u6761\u53D1\u9001</li>\n <li><code>comic random [-n \u6570\u91CF]</code> - \u968F\u673A\u63A8\u8350\uFF0C\u6BCF\u5E73\u53F0\u9ED8\u8BA45\u4E2A(\u6700\u591A20)\uFF0C\u53CC\u5E73\u53F0\u52062\u6761\u53D1\u9001</li>\n</ul>\n";
|
|
16
31
|
export interface Config {
|
|
@@ -20,6 +35,11 @@ export interface Config {
|
|
|
20
35
|
pdfPassword?: string;
|
|
21
36
|
pdfSendMethod: 'buffer' | 'file';
|
|
22
37
|
fileSendPath: string;
|
|
38
|
+
tool: {
|
|
39
|
+
enabled: boolean;
|
|
40
|
+
name: string;
|
|
41
|
+
description: string;
|
|
42
|
+
};
|
|
23
43
|
}
|
|
24
44
|
export declare const Config: Schema<Schemastery.ObjectS<{
|
|
25
45
|
apiBase: Schema<string, string>;
|
|
@@ -28,6 +48,15 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
28
48
|
pdfPassword: Schema<string, string>;
|
|
29
49
|
pdfSendMethod: Schema<"buffer" | "file", "buffer" | "file">;
|
|
30
50
|
fileSendPath: Schema<string, string>;
|
|
51
|
+
tool: Schema<Schemastery.ObjectS<{
|
|
52
|
+
enabled: Schema<boolean, boolean>;
|
|
53
|
+
name: Schema<string, string>;
|
|
54
|
+
description: Schema<string, string>;
|
|
55
|
+
}>, Schemastery.ObjectT<{
|
|
56
|
+
enabled: Schema<boolean, boolean>;
|
|
57
|
+
name: Schema<string, string>;
|
|
58
|
+
description: Schema<string, string>;
|
|
59
|
+
}>>;
|
|
31
60
|
}>, Schemastery.ObjectT<{
|
|
32
61
|
apiBase: Schema<string, string>;
|
|
33
62
|
concurrency: Schema<number, number>;
|
|
@@ -35,6 +64,15 @@ export declare const Config: Schema<Schemastery.ObjectS<{
|
|
|
35
64
|
pdfPassword: Schema<string, string>;
|
|
36
65
|
pdfSendMethod: Schema<"buffer" | "file", "buffer" | "file">;
|
|
37
66
|
fileSendPath: Schema<string, string>;
|
|
67
|
+
tool: Schema<Schemastery.ObjectS<{
|
|
68
|
+
enabled: Schema<boolean, boolean>;
|
|
69
|
+
name: Schema<string, string>;
|
|
70
|
+
description: Schema<string, string>;
|
|
71
|
+
}>, Schemastery.ObjectT<{
|
|
72
|
+
enabled: Schema<boolean, boolean>;
|
|
73
|
+
name: Schema<string, string>;
|
|
74
|
+
description: Schema<string, string>;
|
|
75
|
+
}>>;
|
|
38
76
|
}>>;
|
|
39
77
|
export declare function apply(ctx: Context, config: Config): void;
|
|
40
78
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAA;AASpD,OAAO,QAAQ,QAAQ,CAAC;IACtB,UAAU,OAAO;QACf,QAAQ,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,EAAc,MAAM,QAAQ,CAAA;AASpD,OAAO,QAAQ,QAAQ,CAAC;IACtB,UAAU,OAAO;QACf,QAAQ,EAAE;YACR,QAAQ,EAAE;gBACR,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;oBAClC,WAAW,EAAE,MAAM,CAAA;oBACnB,QAAQ,EAAE,MAAM,OAAO,CAAA;oBACvB,UAAU,EAAE,MAAM,GAAG,CAAA;oBACrB,IAAI,EAAE;wBACJ,MAAM,EAAE,MAAM,CAAA;wBACd,KAAK,EAAE,MAAM,CAAA;wBACb,IAAI,EAAE,MAAM,EAAE,CAAA;wBACd,mBAAmB,EAAE;4BACnB,OAAO,EAAE,OAAO,CAAA;4BAChB,IAAI,EAAE,OAAO,CAAA;4BACb,QAAQ,EAAE,OAAO,CAAA;4BACjB,cAAc,EAAE,MAAM,CAAA;yBACvB,CAAA;qBACF,CAAA;iBACF,GAAG,MAAM,IAAI,CAAA;aACf,CAAA;YACD,eAAe,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAA;SAC9C,CAAA;KACF;CACF;AAED,eAAO,MAAM,MAAM;;CAElB,CAAA;AAED,eAAO,MAAM,KAAK,slDAcjB,CAAA;AAED,MAAM,WAAW,MAAM;IACrB,OAAO,EAAE,MAAM,CAAA;IACf,WAAW,EAAE,MAAM,CAAA;IACnB,OAAO,EAAE,OAAO,CAAA;IAChB,WAAW,CAAC,EAAE,MAAM,CAAA;IACpB,aAAa,EAAE,QAAQ,GAAG,MAAM,CAAA;IAChC,YAAY,EAAE,MAAM,CAAA;IACpB,IAAI,EAAE;QACJ,OAAO,EAAE,OAAO,CAAA;QAChB,IAAI,EAAE,MAAM,CAAA;QACZ,WAAW,EAAE,MAAM,CAAA;KACpB,CAAA;CACF;AAED,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BjB,CAAA;AA8DF,wBAAgB,KAAK,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,QAyhBjD"}
|
package/lib/index.js
CHANGED
|
@@ -43,8 +43,7 @@ const crypto_1 = require("crypto");
|
|
|
43
43
|
const tools_1 = require("@langchain/core/tools");
|
|
44
44
|
const zod_1 = require("zod");
|
|
45
45
|
exports.inject = {
|
|
46
|
-
required: ['http'],
|
|
47
|
-
optional: ['chatluna'],
|
|
46
|
+
required: ['http', 'chatluna'],
|
|
48
47
|
};
|
|
49
48
|
exports.usage = `
|
|
50
49
|
<h2>聚合漫画搜索与下载插件</h2>
|
|
@@ -80,6 +79,13 @@ exports.Config = koishi_1.Schema.object({
|
|
|
80
79
|
fileSendPath: koishi_1.Schema.string()
|
|
81
80
|
.description('PDF 发送方式为 file 时的本地中转保存目录')
|
|
82
81
|
.default('/koishi/temp'),
|
|
82
|
+
tool: koishi_1.Schema.object({
|
|
83
|
+
enabled: koishi_1.Schema.boolean().default(true).description('开启后自动注册 ChatLuna 工具'),
|
|
84
|
+
name: koishi_1.Schema.string().default('comic').description('工具名称'),
|
|
85
|
+
description: koishi_1.Schema.string()
|
|
86
|
+
.default('漫画搜索与下载工具。支持禁漫天堂(JM)和哔咔漫画(Bika)双平台。包含动作:search (搜索), detail (查看详情), leaderboard (排行榜), latest (最近更新), random (随机推荐), download (下载本子并发送)。提示:如果用户给出的本子名、角色名或关键词模糊、包含缩写或拼写不够精确,或者使用该工具搜索未返回结果,你必须先调用联网搜索工具检索获取该本子的准确正式名称、画师/作者或完整标题,然后再用精准的关键词调用此工具。')
|
|
87
|
+
.description('工具描述'),
|
|
88
|
+
}).description('ChatLuna 工具设置'),
|
|
83
89
|
});
|
|
84
90
|
function getPdfPassword(source, comicId, chapterId) {
|
|
85
91
|
const src = source.trim().toLowerCase();
|
|
@@ -94,23 +100,15 @@ function sanitizeFilename(name) {
|
|
|
94
100
|
function determineSource(id) {
|
|
95
101
|
const clean = id.trim();
|
|
96
102
|
const hasJm = /jm/i.test(clean) || /禁漫/i.test(clean);
|
|
97
|
-
const hasBika = /bika/i.test(clean) || /哔咔/i.test(clean);
|
|
98
103
|
const digitMatch = clean.match(/\d+/);
|
|
99
|
-
const hex24Match = clean.match(/[0-9a-fA-F]{24}/);
|
|
100
|
-
if (hasBika && hex24Match) {
|
|
101
|
-
return { source: 'bika', id: hex24Match[0] };
|
|
102
|
-
}
|
|
103
104
|
if (hasJm && digitMatch) {
|
|
104
105
|
return { source: 'jm', id: digitMatch[0] };
|
|
105
106
|
}
|
|
106
|
-
if (/^[0-9a-fA-F]{24}$/.test(clean)) {
|
|
107
|
-
return { source: 'bika', id: clean };
|
|
108
|
-
}
|
|
109
107
|
if (/^\d+$/.test(clean)) {
|
|
110
108
|
return { source: 'jm', id: clean };
|
|
111
109
|
}
|
|
112
|
-
if (
|
|
113
|
-
return { source: 'bika', id:
|
|
110
|
+
if (/^[0-9a-fA-F]{24}$/.test(clean)) {
|
|
111
|
+
return { source: 'bika', id: clean };
|
|
114
112
|
}
|
|
115
113
|
return null;
|
|
116
114
|
}
|
|
@@ -136,18 +134,8 @@ function apply(ctx, config) {
|
|
|
136
134
|
return res;
|
|
137
135
|
}
|
|
138
136
|
catch (err) {
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (body) {
|
|
142
|
-
if (typeof body === 'object') {
|
|
143
|
-
errMsg = body.detail || body.error || errMsg;
|
|
144
|
-
}
|
|
145
|
-
else if (typeof body === 'string') {
|
|
146
|
-
errMsg = body;
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
logger.error(`API 请求失败 [${url}]: ${errMsg}`);
|
|
150
|
-
throw new Error(errMsg);
|
|
137
|
+
logger.error(`API 请求失败 [${url}]: ${err.message}`);
|
|
138
|
+
throw err;
|
|
151
139
|
}
|
|
152
140
|
}
|
|
153
141
|
// 子命令关键词集合,用于父命令路由时排除(避免被当作搜索词/下载ID)
|
|
@@ -170,16 +158,12 @@ function apply(ctx, config) {
|
|
|
170
158
|
const rest = parts.slice(1).join(' ');
|
|
171
159
|
return session.execute(`comic.${firstWord}${rest ? ' ' + rest : ''}`);
|
|
172
160
|
}
|
|
173
|
-
const
|
|
174
|
-
if (
|
|
175
|
-
if (
|
|
161
|
+
const resolved = determineSource(clean);
|
|
162
|
+
if (resolved) {
|
|
163
|
+
if (resolved.source === 'jm' && parseInt(resolved.id, 10) <= 100) {
|
|
176
164
|
return session.execute(`comic.search ${keyword}`);
|
|
177
165
|
}
|
|
178
|
-
|
|
179
|
-
const downloadCmd = chapterId
|
|
180
|
-
? `comic.download ${resolvedFirst.id} ${chapterId}`
|
|
181
|
-
: `comic.download ${resolvedFirst.id}`;
|
|
182
|
-
return session.execute(downloadCmd);
|
|
166
|
+
return session.execute(`comic.download ${resolved.id}`);
|
|
183
167
|
}
|
|
184
168
|
// Fallback to search
|
|
185
169
|
return session.execute(`comic.search ${keyword}`);
|
|
@@ -347,6 +331,7 @@ function apply(ctx, config) {
|
|
|
347
331
|
targetId = resolved.id;
|
|
348
332
|
}
|
|
349
333
|
else {
|
|
334
|
+
await session.send(`正在搜索并解析本子名「${id}」...`);
|
|
350
335
|
try {
|
|
351
336
|
const searchResult = await apiGet('/api/search', { keyword: id });
|
|
352
337
|
const jmList = searchResult.all_results?.jm || [];
|
|
@@ -368,6 +353,7 @@ function apply(ctx, config) {
|
|
|
368
353
|
}
|
|
369
354
|
}
|
|
370
355
|
try {
|
|
356
|
+
await session.send('正在获取漫画详情...');
|
|
371
357
|
let detail;
|
|
372
358
|
try {
|
|
373
359
|
detail = await apiGet(`/api/comic/${source}/${targetId}`);
|
|
@@ -388,6 +374,10 @@ function apply(ctx, config) {
|
|
|
388
374
|
return `未找到章节 ${chapterId}`;
|
|
389
375
|
const chapterName = chapter.name;
|
|
390
376
|
const totalChapters = detail.chapters.length;
|
|
377
|
+
if (!chapterId && totalChapters > 1) {
|
|
378
|
+
await session.send(`该漫画共 ${totalChapters} 话,正在下载第一话: ${chapterName}`);
|
|
379
|
+
}
|
|
380
|
+
await session.send(`正在下载「${detail.title} - ${chapterName}」...`);
|
|
391
381
|
const pdfPassword = config.pdfPassword || getPdfPassword(source, targetId, chapter.id);
|
|
392
382
|
const downloadUrl = config.apiBase.replace(/\/+$/, '')
|
|
393
383
|
+ `/api/download/${source}/${targetId}/${chapter.id}`
|
|
@@ -411,21 +401,21 @@ function apply(ctx, config) {
|
|
|
411
401
|
const tempFilePath = path.join(tempDir, filename);
|
|
412
402
|
await fs.promises.writeFile(tempFilePath, buffer);
|
|
413
403
|
const fileUrl = (0, url_1.pathToFileURL)(tempFilePath).href;
|
|
414
|
-
await session.send(koishi_1.h.file(fileUrl
|
|
404
|
+
await session.send(koishi_1.h.file(fileUrl));
|
|
415
405
|
}
|
|
416
406
|
else {
|
|
417
|
-
await session.send(koishi_1.h.file(buffer, 'application/pdf', {
|
|
407
|
+
await session.send(koishi_1.h.file(buffer, 'application/pdf', { filename }));
|
|
418
408
|
}
|
|
409
|
+
await session.send(`🔑 该 PDF 已加密,解密密码为:${pdfPassword}`);
|
|
419
410
|
}
|
|
420
411
|
catch (sendErr) {
|
|
421
412
|
logger.warn('发送 PDF 文件失败,尝试提供直接下载链接:', sendErr.message);
|
|
422
|
-
await session.send(`⚠️ 发送 PDF 文件失败,请通过以下链接直接下载:\n🔗 ${downloadUrl}`);
|
|
413
|
+
await session.send(`⚠️ 发送 PDF 文件失败,请通过以下链接直接下载:\n🔗 ${downloadUrl}\n🔑 解密密码为:${pdfPassword}`);
|
|
423
414
|
}
|
|
424
|
-
const successMsg = `已下载「${detail.title} - ${chapterName}」,密码为:${pdfPassword}`;
|
|
425
415
|
if (totalChapters > 1 && !chapterId) {
|
|
426
|
-
return `${
|
|
416
|
+
return `这是第一话,共 ${totalChapters} 话。如需其他话请指定章节ID。\n章节列表:\n${detail.chapters.slice(0, 20).map((ch, i) => `${i + 1}. [${ch.id}] ${ch.name}`).join('\n')}${totalChapters > 20 ? `\n... 共 ${totalChapters} 话` : ''}`;
|
|
427
417
|
}
|
|
428
|
-
return
|
|
418
|
+
return '下载完成!';
|
|
429
419
|
}
|
|
430
420
|
catch (err) {
|
|
431
421
|
logger.error('下载失败:', err.message);
|
|
@@ -536,8 +526,7 @@ function apply(ctx, config) {
|
|
|
536
526
|
.action(async ({ session, options }) => {
|
|
537
527
|
if (!session)
|
|
538
528
|
return '此命令仅支持在会话中使用';
|
|
539
|
-
const
|
|
540
|
-
const limit = Math.min(50, Math.max(1, Math.floor(count)));
|
|
529
|
+
const limit = Math.min(50, Math.max(1, Math.floor(options?.number || 10)));
|
|
541
530
|
try {
|
|
542
531
|
const [jmItems, bikaItems] = await Promise.all([
|
|
543
532
|
fetchUpTo('jm', 'latest', limit).catch(() => []),
|
|
@@ -585,8 +574,7 @@ function apply(ctx, config) {
|
|
|
585
574
|
.alias('随机漫画')
|
|
586
575
|
.option('number', '-n <count:number> 每个平台推荐数量(默认5,最多20)')
|
|
587
576
|
.action(async ({ session, options }) => {
|
|
588
|
-
const
|
|
589
|
-
const limit = Math.min(20, Math.max(1, Math.floor(count)));
|
|
577
|
+
const limit = Math.min(20, Math.max(1, Math.floor(options?.number || 5)));
|
|
590
578
|
const [jmItems, bikaItems] = await Promise.all([
|
|
591
579
|
fetchRandomUpTo('jm', limit).catch(() => []),
|
|
592
580
|
fetchRandomUpTo('bika', limit).catch(() => []),
|
|
@@ -610,164 +598,154 @@ function apply(ctx, config) {
|
|
|
610
598
|
});
|
|
611
599
|
// ========== ChatLuna 工具注册 ==========
|
|
612
600
|
ctx.on('ready', async () => {
|
|
613
|
-
if (!
|
|
614
|
-
logger.info('chatluna 未安装,跳过注册 ChatLuna 工具');
|
|
601
|
+
if (!config.tool.enabled)
|
|
615
602
|
return;
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
603
|
+
const toolName = (config.tool.name || 'comic').trim() || 'comic';
|
|
604
|
+
ctx.effect(() => ctx.chatluna.platform.registerTool(toolName, {
|
|
605
|
+
description: config.tool.description || '漫画搜索与下载工具',
|
|
606
|
+
selector() {
|
|
607
|
+
return true;
|
|
608
|
+
},
|
|
609
|
+
createTool() {
|
|
610
|
+
return createComicTool(ctx, config);
|
|
611
|
+
},
|
|
612
|
+
meta: {
|
|
613
|
+
source: 'extension',
|
|
614
|
+
group: 'comic',
|
|
615
|
+
tags: ['comic', 'manga', 'jmcomic', 'bika'],
|
|
616
|
+
defaultAvailability: {
|
|
617
|
+
enabled: true,
|
|
618
|
+
main: true,
|
|
619
|
+
chatluna: true,
|
|
620
|
+
characterScope: 'all',
|
|
623
621
|
},
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
622
|
+
},
|
|
623
|
+
}));
|
|
624
|
+
logger.info(`ChatLuna 工具「${toolName}」已注册`);
|
|
625
|
+
});
|
|
626
|
+
}
|
|
627
|
+
const comicToolSchema = zod_1.z.object({
|
|
628
|
+
action: zod_1.z.enum(['search', 'detail', 'leaderboard', 'latest', 'random', 'download'])
|
|
629
|
+
.describe('操作类型:search=搜索, detail=详情, leaderboard=排行榜, latest=最近更新, random=随机推荐, download=下载漫画/本子并发送给用户'),
|
|
630
|
+
keyword: zod_1.z.string().optional()
|
|
631
|
+
.describe('搜索关键词(action=search 时必填)'),
|
|
632
|
+
source: zod_1.z.enum(['jm', 'bika']).optional()
|
|
633
|
+
.describe('漫画源:jm=禁漫天堂, bika=哔咔漫画。不填则根据需要自动检测或使用默认源'),
|
|
634
|
+
comic_id: zod_1.z.string().optional()
|
|
635
|
+
.describe('漫画ID(action=detail 或 action=download 时必填)'),
|
|
636
|
+
chapter_id: zod_1.z.string().optional()
|
|
637
|
+
.describe('章节ID(action=download 时选填,不填默认下载第一话)'),
|
|
638
|
+
mode: zod_1.z.enum(['day', 'week', 'month', 'total']).optional()
|
|
639
|
+
.describe('排行榜时间维度(action=leaderboard 时使用):day=日榜, week=周榜, month=月榜, total=总榜'),
|
|
640
|
+
page: zod_1.z.number().int().min(1).optional()
|
|
641
|
+
.describe('页码,默认1'),
|
|
642
|
+
});
|
|
643
|
+
function createComicTool(ctx, cfg) {
|
|
644
|
+
const description = (cfg.tool.description || '').trim() || '漫画搜索与下载工具。支持禁漫天堂(JM)和哔咔漫画(Bika)双平台。包含动作:search (搜索), detail (查看详情), leaderboard (排行榜), latest (最近更新), random (随机推荐), download (下载本子并发送)。提示:如果用户给出的本子名、角色名或关键词模糊、包含缩写或拼写不够精确,或者使用该工具搜索未返回结果,你必须先调用联网搜索工具检索获取该本子的准确正式名称、画师/作者或完整标题,然后再用精准的关键词调用此工具。';
|
|
645
|
+
const name = (cfg.tool.name || 'comic').trim() || 'comic';
|
|
646
|
+
return (0, tools_1.tool)(async (input, runConfig) => {
|
|
647
|
+
const logger = ctx.logger('comic');
|
|
648
|
+
const source = input.source || 'jm';
|
|
649
|
+
const apiBase = cfg.apiBase.replace(/\/+$/, '');
|
|
650
|
+
const apiGet = async (path, params) => {
|
|
651
|
+
let url = apiBase + path;
|
|
652
|
+
if (params) {
|
|
653
|
+
const qs = new URLSearchParams(params).toString();
|
|
654
|
+
if (qs)
|
|
655
|
+
url += '?' + qs;
|
|
656
|
+
}
|
|
657
|
+
return ctx.http.get(url);
|
|
640
658
|
};
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
keyword: input.keyword,
|
|
655
|
-
best_match: result.best_match,
|
|
656
|
-
total: all.length,
|
|
657
|
-
results: all.slice(0, 20),
|
|
658
|
-
});
|
|
659
|
-
}
|
|
660
|
-
catch (err) {
|
|
661
|
-
logger.error('工具 comic_search 调用失败:', err.message);
|
|
662
|
-
return JSON.stringify({ error: err.message || '请求失败' });
|
|
663
|
-
}
|
|
664
|
-
});
|
|
665
|
-
// 2. comic_detail
|
|
666
|
-
registerSingleTool('comic_detail', '查看指定来源和ID的漫画详情,包括标题、作者、简介以及可下载的章节列表。', zod_1.z.object({
|
|
667
|
-
comic_id: zod_1.z.string().describe('漫画ID'),
|
|
668
|
-
source: zod_1.z.enum(['jm', 'bika']).optional().default('jm').describe('漫画源:jm=禁漫天堂, bika=哔咔漫画。默认jm。'),
|
|
669
|
-
}), async (input) => {
|
|
670
|
-
try {
|
|
671
|
-
const source = input.source || 'jm';
|
|
672
|
-
const detail = await apiGet(`/api/comic/${source}/${input.comic_id}`);
|
|
673
|
-
if (!detail?.title) {
|
|
674
|
-
return JSON.stringify({ error: '未找到该漫画' });
|
|
659
|
+
try {
|
|
660
|
+
switch (input.action) {
|
|
661
|
+
case 'download': {
|
|
662
|
+
if (!input.comic_id) {
|
|
663
|
+
return JSON.stringify({ error: '下载漫画时 comic_id 不能为空' });
|
|
664
|
+
}
|
|
665
|
+
const session = runConfig?.configurable?.session;
|
|
666
|
+
if (!session) {
|
|
667
|
+
return JSON.stringify({ error: '无法获取当前会话,不支持下载操作。请让用户在聊天界面直接使用 comic 命令或 comic.download 命令手动下载。' });
|
|
668
|
+
}
|
|
669
|
+
const cmd = input.chapter_id ? `comic.download ${input.comic_id} ${input.chapter_id}` : `comic.download ${input.comic_id}`;
|
|
670
|
+
session.execute(cmd);
|
|
671
|
+
return JSON.stringify({ success: true, message: `已在后台启动漫画下载,命令为: ${cmd}。请告知用户正在下载,并让其留意后续接收的 PDF 文件与密码。` });
|
|
675
672
|
}
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
mode: zod_1.z.enum(['day', 'week', 'month', 'total']).optional().default('day').describe('排行榜时间维度:day=日榜, week=周榜, month=月榜, total=总榜。默认day。'),
|
|
694
|
-
page: zod_1.z.number().int().min(1).optional().default(1).describe('页码,默认1。'),
|
|
695
|
-
}), async (input) => {
|
|
696
|
-
try {
|
|
697
|
-
const source = input.source || 'jm';
|
|
698
|
-
const mode = input.mode || 'day';
|
|
699
|
-
const page = String(input.page || 1);
|
|
700
|
-
const result = await apiGet(`/api/${source}/leaderboard`, { mode, page });
|
|
701
|
-
return JSON.stringify({
|
|
702
|
-
source,
|
|
703
|
-
mode,
|
|
704
|
-
page: Number(page),
|
|
705
|
-
total: result.data?.length || 0,
|
|
706
|
-
results: result.data || [],
|
|
707
|
-
});
|
|
708
|
-
}
|
|
709
|
-
catch (err) {
|
|
710
|
-
logger.error('工具 comic_leaderboard 调用失败:', err.message);
|
|
711
|
-
return JSON.stringify({ error: err.message || '请求失败' });
|
|
712
|
-
}
|
|
713
|
-
});
|
|
714
|
-
// 4. comic_latest
|
|
715
|
-
registerSingleTool('comic_latest', '查看指定漫画源的最近更新列表。', zod_1.z.object({
|
|
716
|
-
source: zod_1.z.enum(['jm', 'bika']).optional().default('jm').describe('漫画源:jm=禁漫天堂, bika=哔咔漫画。默认jm。'),
|
|
717
|
-
page: zod_1.z.number().int().min(1).optional().default(1).describe('页码,默认1。'),
|
|
718
|
-
}), async (input) => {
|
|
719
|
-
try {
|
|
720
|
-
const source = input.source || 'jm';
|
|
721
|
-
const page = String(input.page || 1);
|
|
722
|
-
const result = await apiGet(`/api/${source}/latest`, { page });
|
|
723
|
-
return JSON.stringify({
|
|
724
|
-
source,
|
|
725
|
-
page: Number(page),
|
|
726
|
-
total: result.data?.length || 0,
|
|
727
|
-
results: result.data || [],
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
catch (err) {
|
|
731
|
-
logger.error('工具 comic_latest 调用失败:', err.message);
|
|
732
|
-
return JSON.stringify({ error: err.message || '请求失败' });
|
|
733
|
-
}
|
|
734
|
-
});
|
|
735
|
-
// 5. comic_random
|
|
736
|
-
registerSingleTool('comic_random', '随机推荐指定漫画源的漫画。', zod_1.z.object({
|
|
737
|
-
source: zod_1.z.enum(['jm', 'bika']).optional().default('jm').describe('漫画源:jm=禁漫天堂, bika=哔咔漫画。默认jm。'),
|
|
738
|
-
}), async (input) => {
|
|
739
|
-
try {
|
|
740
|
-
const source = input.source || 'jm';
|
|
741
|
-
const result = await apiGet(`/api/${source}/random`);
|
|
742
|
-
return JSON.stringify({
|
|
743
|
-
source,
|
|
744
|
-
result: result.data || null,
|
|
745
|
-
});
|
|
746
|
-
}
|
|
747
|
-
catch (err) {
|
|
748
|
-
logger.error('工具 comic_random 调用失败:', err.message);
|
|
749
|
-
return JSON.stringify({ error: err.message || '请求失败' });
|
|
750
|
-
}
|
|
751
|
-
});
|
|
752
|
-
// 6. comic_download
|
|
753
|
-
registerSingleTool('comic_download', '下载指定ID的漫画并发送给用户。会自动启动后台下载并向用户发送PDF文件及解密密码。', zod_1.z.object({
|
|
754
|
-
comic_id: zod_1.z.string().describe('漫画ID'),
|
|
755
|
-
chapter_id: zod_1.z.string().optional().describe('章节ID(选填,不填默认下载第一话)'),
|
|
756
|
-
}), async (input, runConfig) => {
|
|
757
|
-
try {
|
|
758
|
-
const session = runConfig?.configurable?.session;
|
|
759
|
-
if (!session) {
|
|
760
|
-
return JSON.stringify({ error: '无法获取当前会话,不支持下载操作。请让用户在聊天界面直接使用 comic 命令或 comic.download 命令手动下载。' });
|
|
673
|
+
case 'search': {
|
|
674
|
+
if (!input.keyword) {
|
|
675
|
+
return JSON.stringify({ error: '搜索时 keyword 不能为空' });
|
|
676
|
+
}
|
|
677
|
+
const result = await apiGet('/api/search', { keyword: input.keyword });
|
|
678
|
+
const jmList = result.all_results?.jm || [];
|
|
679
|
+
const bikaList = result.all_results?.bika || [];
|
|
680
|
+
const all = [
|
|
681
|
+
...jmList.map(c => ({ ...c, source: 'jm' })),
|
|
682
|
+
...bikaList.map(c => ({ ...c, source: 'bika' })),
|
|
683
|
+
];
|
|
684
|
+
return JSON.stringify({
|
|
685
|
+
keyword: input.keyword,
|
|
686
|
+
best_match: result.best_match,
|
|
687
|
+
total: all.length,
|
|
688
|
+
results: all.slice(0, 20),
|
|
689
|
+
});
|
|
761
690
|
}
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
691
|
+
case 'detail': {
|
|
692
|
+
if (!input.comic_id) {
|
|
693
|
+
return JSON.stringify({ error: '查看详情时 comic_id 不能为空' });
|
|
694
|
+
}
|
|
695
|
+
const detail = await apiGet(`/api/comic/${source}/${input.comic_id}`);
|
|
696
|
+
if (!detail?.title) {
|
|
697
|
+
return JSON.stringify({ error: '未找到该漫画' });
|
|
698
|
+
}
|
|
699
|
+
return JSON.stringify({
|
|
700
|
+
title: detail.title,
|
|
701
|
+
author: detail.author,
|
|
702
|
+
description: detail.description,
|
|
703
|
+
source: detail.source || source,
|
|
704
|
+
chapter_count: detail.chapters?.length || 0,
|
|
705
|
+
chapters: detail.chapters?.slice(0, 30) || [],
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
case 'leaderboard': {
|
|
709
|
+
const mode = input.mode || 'day';
|
|
710
|
+
const page = String(input.page || 1);
|
|
711
|
+
const result = await apiGet(`/api/${source}/leaderboard`, { mode, page });
|
|
712
|
+
return JSON.stringify({
|
|
713
|
+
source,
|
|
714
|
+
mode,
|
|
715
|
+
page: Number(page),
|
|
716
|
+
total: result.data?.length || 0,
|
|
717
|
+
results: result.data || [],
|
|
718
|
+
});
|
|
719
|
+
}
|
|
720
|
+
case 'latest': {
|
|
721
|
+
const page = String(input.page || 1);
|
|
722
|
+
const result = await apiGet(`/api/${source}/latest`, { page });
|
|
723
|
+
return JSON.stringify({
|
|
724
|
+
source,
|
|
725
|
+
page: Number(page),
|
|
726
|
+
total: result.data?.length || 0,
|
|
727
|
+
results: result.data || [],
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
case 'random': {
|
|
731
|
+
const result = await apiGet(`/api/${source}/random`);
|
|
732
|
+
return JSON.stringify({
|
|
733
|
+
source,
|
|
734
|
+
result: result.data || null,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
default:
|
|
738
|
+
return JSON.stringify({ error: '未知操作类型' });
|
|
769
739
|
}
|
|
770
|
-
}
|
|
740
|
+
}
|
|
741
|
+
catch (err) {
|
|
742
|
+
logger.error('工具调用失败:', err.message);
|
|
743
|
+
return JSON.stringify({ error: err.message || '请求失败' });
|
|
744
|
+
}
|
|
745
|
+
}, {
|
|
746
|
+
name,
|
|
747
|
+
description,
|
|
748
|
+
schema: comicToolSchema,
|
|
771
749
|
});
|
|
772
750
|
}
|
|
773
751
|
//# sourceMappingURL=index.js.map
|