koishi-plugin-cfmrmod 1.1.8 → 1.1.10
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/cfmr/index.js +50 -51
- package/dist/match.js +134 -0
- package/dist/mcmod/cards/comment-card.js +266 -21
- package/dist/mcmod/cards.js +3 -1
- package/dist/mcmod/plugin.js +180 -29
- package/dist/mcmod/search.js +55 -0
- package/dist/nlu.js +70 -9
- package/package.json +1 -1
package/dist/cfmr/index.js
CHANGED
|
@@ -12,6 +12,7 @@ const { Schema, h } = require('koishi');
|
|
|
12
12
|
const fetch = require('node-fetch');
|
|
13
13
|
const cheerio = require('cheerio');
|
|
14
14
|
const { marked } = require('marked');
|
|
15
|
+
const match_1 = require("../match");
|
|
15
16
|
let createCanvas;
|
|
16
17
|
let loadImage;
|
|
17
18
|
let Path2DRef;
|
|
@@ -2478,7 +2479,30 @@ function apply(ctx, config) {
|
|
|
2478
2479
|
list.map((item, i) => `${i + 1 + page * size}. [${item.platform}] ${item.name} - ${item.author}`).join('\n') +
|
|
2479
2480
|
'\n请输入序号查看详情 (p/n 翻页, q 退出)';
|
|
2480
2481
|
};
|
|
2481
|
-
const
|
|
2482
|
+
const renderProjectResult = async (session, item, useEnglish = false) => {
|
|
2483
|
+
let detailData;
|
|
2484
|
+
if (item.platform === 'Modrinth')
|
|
2485
|
+
detailData = await fetchModrinthDetail(item.id, config.requestTimeout);
|
|
2486
|
+
else
|
|
2487
|
+
detailData = await fetchCurseForgeDetail(item.id, config.curseforgeApiKey, config.requestTimeout, item._cfUrl);
|
|
2488
|
+
detailData.type = item.type;
|
|
2489
|
+
detailData._lang = useEnglish ? 'en' : 'zh';
|
|
2490
|
+
const imgBufs = detailData.source === 'CurseForge'
|
|
2491
|
+
? await drawProjectCardCF({
|
|
2492
|
+
...detailData,
|
|
2493
|
+
maxCanvasHeight: config.maxCanvasHeight || 8000
|
|
2494
|
+
})
|
|
2495
|
+
: await drawProjectCard({
|
|
2496
|
+
...detailData,
|
|
2497
|
+
maxCanvasHeight: config.maxCanvasHeight || 8000
|
|
2498
|
+
});
|
|
2499
|
+
for (const buf of imgBufs) {
|
|
2500
|
+
await session.send(h.image(await toImageSrc(buf)));
|
|
2501
|
+
}
|
|
2502
|
+
if (config.sendLink)
|
|
2503
|
+
await session.send(`${useEnglish ? 'Link' : '链接'}: ${detailData.url}`);
|
|
2504
|
+
};
|
|
2505
|
+
const handleSearch = async (session, platform, type, keyword, useEnglish = false, direct = false) => {
|
|
2482
2506
|
if (!keyword) {
|
|
2483
2507
|
await session.send(useEnglish ? 'Please enter a keyword' : '请输入关键词');
|
|
2484
2508
|
return;
|
|
@@ -2498,30 +2522,21 @@ function apply(ctx, config) {
|
|
|
2498
2522
|
await session.send(useEnglish ? 'No results found' : '未找到结果');
|
|
2499
2523
|
return;
|
|
2500
2524
|
}
|
|
2525
|
+
const directItem = direct ? (0, match_1.selectExactSearchResult)(results, keyword) : null;
|
|
2526
|
+
if (directItem) {
|
|
2527
|
+
try {
|
|
2528
|
+
await renderProjectResult(session, directItem, useEnglish);
|
|
2529
|
+
}
|
|
2530
|
+
catch (e) {
|
|
2531
|
+
logger.error(e);
|
|
2532
|
+
return session.send(`${useEnglish ? 'Generation failed' : '生成失败'}: ${e.message}`);
|
|
2533
|
+
}
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2501
2536
|
if (results.length === 1) {
|
|
2502
2537
|
const item = results[0];
|
|
2503
2538
|
try {
|
|
2504
|
-
|
|
2505
|
-
if (item.platform === 'Modrinth')
|
|
2506
|
-
detailData = await fetchModrinthDetail(item.id, config.requestTimeout);
|
|
2507
|
-
else
|
|
2508
|
-
detailData = await fetchCurseForgeDetail(item.id, config.curseforgeApiKey, config.requestTimeout, item._cfUrl);
|
|
2509
|
-
detailData.type = item.type;
|
|
2510
|
-
detailData._lang = useEnglish ? 'en' : 'zh';
|
|
2511
|
-
const imgBufs = detailData.source === 'CurseForge'
|
|
2512
|
-
? await drawProjectCardCF({
|
|
2513
|
-
...detailData,
|
|
2514
|
-
maxCanvasHeight: config.maxCanvasHeight || 8000
|
|
2515
|
-
})
|
|
2516
|
-
: await drawProjectCard({
|
|
2517
|
-
...detailData,
|
|
2518
|
-
maxCanvasHeight: config.maxCanvasHeight || 8000
|
|
2519
|
-
});
|
|
2520
|
-
for (const buf of imgBufs) {
|
|
2521
|
-
await session.send(h.image(await toImageSrc(buf)));
|
|
2522
|
-
}
|
|
2523
|
-
if (config.sendLink)
|
|
2524
|
-
await session.send(`${useEnglish ? 'Link' : '链接'}: ${detailData.url}`);
|
|
2539
|
+
await renderProjectResult(session, item, useEnglish);
|
|
2525
2540
|
}
|
|
2526
2541
|
catch (e) {
|
|
2527
2542
|
logger.error(e);
|
|
@@ -2564,27 +2579,7 @@ function apply(ctx, config) {
|
|
|
2564
2579
|
await tryWithdraw(session, state.listMessageIds);
|
|
2565
2580
|
states.delete(session.cid);
|
|
2566
2581
|
try {
|
|
2567
|
-
|
|
2568
|
-
if (item.platform === 'Modrinth')
|
|
2569
|
-
detailData = await fetchModrinthDetail(item.id, config.requestTimeout);
|
|
2570
|
-
else
|
|
2571
|
-
detailData = await fetchCurseForgeDetail(item.id, config.curseforgeApiKey, config.requestTimeout, item._cfUrl);
|
|
2572
|
-
detailData.type = item.type;
|
|
2573
|
-
detailData._lang = useEnglish ? 'en' : 'zh';
|
|
2574
|
-
const imgBufs = detailData.source === 'CurseForge'
|
|
2575
|
-
? await drawProjectCardCF({
|
|
2576
|
-
...detailData,
|
|
2577
|
-
maxCanvasHeight: config.maxCanvasHeight || 8000
|
|
2578
|
-
})
|
|
2579
|
-
: await drawProjectCard({
|
|
2580
|
-
...detailData,
|
|
2581
|
-
maxCanvasHeight: config.maxCanvasHeight || 8000
|
|
2582
|
-
});
|
|
2583
|
-
for (const buf of imgBufs) {
|
|
2584
|
-
await session.send(h.image(await toImageSrc(buf)));
|
|
2585
|
-
}
|
|
2586
|
-
if (config.sendLink)
|
|
2587
|
-
await session.send(`${useEnglish ? 'Link' : '链接'}: ${detailData.url}`);
|
|
2582
|
+
await renderProjectResult(session, item, useEnglish);
|
|
2588
2583
|
}
|
|
2589
2584
|
catch (e) {
|
|
2590
2585
|
logger.error(e);
|
|
@@ -2636,35 +2631,39 @@ function apply(ctx, config) {
|
|
|
2636
2631
|
['mod', 'pack', 'resource', 'shader', 'plugin'].forEach(t => {
|
|
2637
2632
|
ctx.command(`${mrPrefix}.${t} [...keyword]`, `搜索 Modrinth ${t}`)
|
|
2638
2633
|
.option('e', '-e 使用英文', { fallback: false })
|
|
2634
|
+
.option('direct', '-d, --direct 精确命中时直接渲染', { fallback: false })
|
|
2639
2635
|
.action((argv, ...args) => {
|
|
2640
|
-
var _a;
|
|
2636
|
+
var _a, _b;
|
|
2641
2637
|
const allArgs = args.flat().map(String);
|
|
2642
2638
|
const kw = allArgs.join(' ');
|
|
2643
|
-
return handleSearch(argv.session, 'mr', t, kw, ((_a = argv.options) === null || _a === void 0 ? void 0 : _a.e) === true);
|
|
2639
|
+
return handleSearch(argv.session, 'mr', t, kw, ((_a = argv.options) === null || _a === void 0 ? void 0 : _a.e) === true, ((_b = argv.options) === null || _b === void 0 ? void 0 : _b.direct) === true);
|
|
2644
2640
|
});
|
|
2645
2641
|
ctx.command(`${cfPrefix}.${t} [...keyword]`, `搜索 CurseForge ${t}`)
|
|
2646
2642
|
.option('e', '-e 使用英文', { fallback: false })
|
|
2643
|
+
.option('direct', '-d, --direct 精确命中时直接渲染', { fallback: false })
|
|
2647
2644
|
.action((argv, ...args) => {
|
|
2648
|
-
var _a;
|
|
2645
|
+
var _a, _b;
|
|
2649
2646
|
const allArgs = args.flat().map(String);
|
|
2650
2647
|
const kw = allArgs.join(' ');
|
|
2651
|
-
return handleSearch(argv.session, 'cf', t, kw, ((_a = argv.options) === null || _a === void 0 ? void 0 : _a.e) === true);
|
|
2648
|
+
return handleSearch(argv.session, 'cf', t, kw, ((_a = argv.options) === null || _a === void 0 ? void 0 : _a.e) === true, ((_b = argv.options) === null || _b === void 0 ? void 0 : _b.direct) === true);
|
|
2652
2649
|
});
|
|
2653
2650
|
});
|
|
2654
2651
|
ctx.command(`${mrPrefix} [...keyword]`, '搜索 Modrinth 模组')
|
|
2655
2652
|
.option('e', '-e 使用英文', { fallback: false })
|
|
2653
|
+
.option('direct', '-d, --direct 精确命中时直接渲染', { fallback: false })
|
|
2656
2654
|
.action((argv, ...args) => {
|
|
2657
|
-
var _a;
|
|
2655
|
+
var _a, _b;
|
|
2658
2656
|
const allArgs = args.flat().map(String);
|
|
2659
2657
|
const kw = allArgs.join(' ');
|
|
2660
|
-
return handleSearch(argv.session, 'mr', 'mod', kw, ((_a = argv.options) === null || _a === void 0 ? void 0 : _a.e) === true);
|
|
2658
|
+
return handleSearch(argv.session, 'mr', 'mod', kw, ((_a = argv.options) === null || _a === void 0 ? void 0 : _a.e) === true, ((_b = argv.options) === null || _b === void 0 ? void 0 : _b.direct) === true);
|
|
2661
2659
|
});
|
|
2662
2660
|
ctx.command(`${cfPrefix} [...keyword]`, '搜索 CurseForge 模组')
|
|
2663
2661
|
.option('e', '-e 使用英文', { fallback: false })
|
|
2662
|
+
.option('direct', '-d, --direct 精确命中时直接渲染', { fallback: false })
|
|
2664
2663
|
.action((argv, ...args) => {
|
|
2665
|
-
var _a;
|
|
2664
|
+
var _a, _b;
|
|
2666
2665
|
const allArgs = args.flat().map(String);
|
|
2667
2666
|
const kw = allArgs.join(' ');
|
|
2668
|
-
return handleSearch(argv.session, 'cf', 'mod', kw, ((_a = argv.options) === null || _a === void 0 ? void 0 : _a.e) === true);
|
|
2667
|
+
return handleSearch(argv.session, 'cf', 'mod', kw, ((_a = argv.options) === null || _a === void 0 ? void 0 : _a.e) === true, ((_b = argv.options) === null || _b === void 0 ? void 0 : _b.direct) === true);
|
|
2669
2668
|
});
|
|
2670
2669
|
}
|
package/dist/match.js
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeSearchText = normalizeSearchText;
|
|
4
|
+
exports.searchAliases = searchAliases;
|
|
5
|
+
exports.scoreSearchResult = scoreSearchResult;
|
|
6
|
+
exports.selectExactSearchResult = selectExactSearchResult;
|
|
7
|
+
function cleanQueryText(value) {
|
|
8
|
+
let text = String(value || '').trim();
|
|
9
|
+
const intentWords = [
|
|
10
|
+
'帮我', '请', '查询一下', '查一下', '搜索一下', '查询', '搜索', '找一下', '找',
|
|
11
|
+
'模组', '整合包', '资源包', '材质包', '材质', '光影', '插件', '教程', '作者', '用户',
|
|
12
|
+
'评论', '短评', '本体', '本身', '详情', '页面',
|
|
13
|
+
];
|
|
14
|
+
let changed = true;
|
|
15
|
+
while (changed && text) {
|
|
16
|
+
changed = false;
|
|
17
|
+
for (const word of intentWords) {
|
|
18
|
+
if (text.startsWith(word)) {
|
|
19
|
+
text = text.slice(word.length).trim();
|
|
20
|
+
changed = true;
|
|
21
|
+
}
|
|
22
|
+
if (text.endsWith(word)) {
|
|
23
|
+
text = text.slice(0, -word.length).trim();
|
|
24
|
+
changed = true;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
function stripHtml(value) {
|
|
31
|
+
return String(value || '').replace(/<[^>]+>/g, ' ');
|
|
32
|
+
}
|
|
33
|
+
function normalizeSearchText(value) {
|
|
34
|
+
return stripHtml(value)
|
|
35
|
+
.toLowerCase()
|
|
36
|
+
.normalize('NFKC')
|
|
37
|
+
.replace(/&[a-z0-9#]+;/gi, ' ')
|
|
38
|
+
.replace(/[\s"'`‘’“”.,,。::;;!!??()[\]()【】{}<>《》_\-–—+|/\\]+/g, '')
|
|
39
|
+
.trim();
|
|
40
|
+
}
|
|
41
|
+
function words(value) {
|
|
42
|
+
return stripHtml(value).match(/[A-Za-z0-9]+/g) || [];
|
|
43
|
+
}
|
|
44
|
+
function acronym(value) {
|
|
45
|
+
const list = words(value);
|
|
46
|
+
if (list.length < 2)
|
|
47
|
+
return '';
|
|
48
|
+
return list.map(word => word[0]).join('').toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
function pushAlias(aliases, value) {
|
|
51
|
+
const text = stripHtml(value).replace(/\s+/g, ' ').trim();
|
|
52
|
+
if (!text)
|
|
53
|
+
return;
|
|
54
|
+
aliases.push(text);
|
|
55
|
+
}
|
|
56
|
+
function searchAliases(item) {
|
|
57
|
+
var _a;
|
|
58
|
+
const aliases = [];
|
|
59
|
+
const fields = [item === null || item === void 0 ? void 0 : item.name, item === null || item === void 0 ? void 0 : item.title, item === null || item === void 0 ? void 0 : item.modName, item === null || item === void 0 ? void 0 : item.id].filter(Boolean);
|
|
60
|
+
for (const field of fields) {
|
|
61
|
+
const text = stripHtml(field).replace(/\s+/g, ' ').trim();
|
|
62
|
+
pushAlias(aliases, text);
|
|
63
|
+
text.replace(/[\[((【]([^\]))】]+)[\]))】]/g, (_, alias) => {
|
|
64
|
+
pushAlias(aliases, alias);
|
|
65
|
+
return '';
|
|
66
|
+
});
|
|
67
|
+
pushAlias(aliases, text.replace(/[\[((【][^\]))】]+[\]))】]/g, ' '));
|
|
68
|
+
const leading = (_a = text.match(/^\s*[\[((【]([^\]))】]+)[\]))】]/)) === null || _a === void 0 ? void 0 : _a[1];
|
|
69
|
+
if (leading)
|
|
70
|
+
pushAlias(aliases, leading);
|
|
71
|
+
const acro = acronym(text);
|
|
72
|
+
if (acro)
|
|
73
|
+
pushAlias(aliases, acro);
|
|
74
|
+
}
|
|
75
|
+
return Array.from(new Set(aliases));
|
|
76
|
+
}
|
|
77
|
+
function looksLikeShortAcronym(queryNorm) {
|
|
78
|
+
return /^[a-z0-9]{2,5}$/.test(queryNorm);
|
|
79
|
+
}
|
|
80
|
+
function hasAddonWords(value) {
|
|
81
|
+
return /附属|支持|扩展|集成|兼容|插件|addon|addons|integration|integrations|support|supports|plugin|plugins|bee|bees|hider|history|utility|utilities/i
|
|
82
|
+
.test(String(value || ''));
|
|
83
|
+
}
|
|
84
|
+
function scoreSearchResult(item, query) {
|
|
85
|
+
const rawQuery = cleanQueryText(query);
|
|
86
|
+
const queryNorm = normalizeSearchText(rawQuery);
|
|
87
|
+
if (!queryNorm)
|
|
88
|
+
return 0;
|
|
89
|
+
let best = 0;
|
|
90
|
+
const title = String((item === null || item === void 0 ? void 0 : item.name) || (item === null || item === void 0 ? void 0 : item.title) || '');
|
|
91
|
+
const titleNorm = normalizeSearchText(title);
|
|
92
|
+
const aliases = searchAliases(item);
|
|
93
|
+
for (const alias of aliases) {
|
|
94
|
+
const aliasNorm = normalizeSearchText(alias);
|
|
95
|
+
if (!aliasNorm)
|
|
96
|
+
continue;
|
|
97
|
+
if (aliasNorm === queryNorm)
|
|
98
|
+
best = Math.max(best, 1000);
|
|
99
|
+
if (acronym(alias) === queryNorm) {
|
|
100
|
+
const firstWord = normalizeSearchText(words(alias)[0] || '');
|
|
101
|
+
best = Math.max(best, firstWord === queryNorm ? 640 : 940);
|
|
102
|
+
}
|
|
103
|
+
if (aliasNorm.startsWith(queryNorm)) {
|
|
104
|
+
const lengthPenalty = Math.min(180, Math.max(0, aliasNorm.length - queryNorm.length) * 4);
|
|
105
|
+
const base = looksLikeShortAcronym(queryNorm) ? 620 : 820;
|
|
106
|
+
best = Math.max(best, base - lengthPenalty);
|
|
107
|
+
}
|
|
108
|
+
if (queryNorm.length >= 3 && aliasNorm.includes(queryNorm)) {
|
|
109
|
+
const lengthPenalty = Math.min(160, Math.max(0, aliasNorm.length - queryNorm.length) * 2);
|
|
110
|
+
best = Math.max(best, 520 - lengthPenalty);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (titleNorm && titleNorm === queryNorm)
|
|
114
|
+
best = Math.max(best, 1000);
|
|
115
|
+
if (titleNorm && titleNorm.startsWith(queryNorm)) {
|
|
116
|
+
const base = looksLikeShortAcronym(queryNorm) ? 580 : 760;
|
|
117
|
+
best = Math.max(best, base - Math.min(120, titleNorm.length - queryNorm.length));
|
|
118
|
+
}
|
|
119
|
+
if (looksLikeShortAcronym(queryNorm) && hasAddonWords(title))
|
|
120
|
+
best = Math.max(0, best - 260);
|
|
121
|
+
return best;
|
|
122
|
+
}
|
|
123
|
+
function selectExactSearchResult(results, query, minScore = 700) {
|
|
124
|
+
let best = null;
|
|
125
|
+
let bestScore = 0;
|
|
126
|
+
for (const item of results || []) {
|
|
127
|
+
const score = scoreSearchResult(item, query);
|
|
128
|
+
if (score > bestScore) {
|
|
129
|
+
best = item;
|
|
130
|
+
bestScore = score;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return best && bestScore >= minScore ? best : null;
|
|
134
|
+
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fetchMcmodCommentThread = fetchMcmodCommentThread;
|
|
4
|
+
exports.fetchMcmodCommentList = fetchMcmodCommentList;
|
|
5
|
+
exports.drawMcmodCommentList = drawMcmodCommentList;
|
|
4
6
|
exports.drawMcmodCommentThread = drawMcmodCommentThread;
|
|
5
7
|
const cheerio = require('cheerio');
|
|
6
8
|
const constants_1 = require("../constants");
|
|
@@ -10,17 +12,74 @@ const utils_1 = require("../utils");
|
|
|
10
12
|
const COMMENT_ROW_URL = `${constants_1.BASE_URL}/frame/comment/CommentRow/`;
|
|
11
13
|
const COMMENT_REPLY_URL = `${constants_1.BASE_URL}/frame/comment/CommentReply/`;
|
|
12
14
|
const MCMOD_REPLY_API_PAGE_SIZE = 5;
|
|
13
|
-
function
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
(
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
15
|
+
function readPageTitle(html, context) {
|
|
16
|
+
const $ = cheerio.load(html);
|
|
17
|
+
return ((0, utils_1.cleanText)($('meta[property="og:title"]').attr('content') || '') ||
|
|
18
|
+
(0, utils_1.cleanText)($('title').text()).replace(/\s*-\s*MC百科.*$/, '') ||
|
|
19
|
+
context.container);
|
|
20
|
+
}
|
|
21
|
+
function inferCommentContextFromUrl(url) {
|
|
22
|
+
let parsed;
|
|
23
|
+
try {
|
|
24
|
+
parsed = new URL(url);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
return null;
|
|
22
28
|
}
|
|
23
|
-
|
|
29
|
+
const path = parsed.pathname || '';
|
|
30
|
+
const simpleMatch = path.match(/^\/(class|modpack|post|author)\/(\d+)(?:\.html)?\/?$/i);
|
|
31
|
+
if (simpleMatch)
|
|
32
|
+
return { type: simpleMatch[1].toLowerCase(), container: simpleMatch[2] };
|
|
33
|
+
if (/^\/item\//i.test(path)) {
|
|
34
|
+
const itemMatch = path.match(/^\/item\/([^/?#]+)(?:\.html)?\/?$/i);
|
|
35
|
+
if (itemMatch)
|
|
36
|
+
return { type: 'item', container: itemMatch[1] };
|
|
37
|
+
}
|
|
38
|
+
if (/^center\.mcmod\.cn$/i.test(parsed.hostname)) {
|
|
39
|
+
const centerMatch = path.match(/^\/(\d+)\/?$/);
|
|
40
|
+
if (centerMatch)
|
|
41
|
+
return { type: 'center', container: centerMatch[1] };
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
function extractCommentContext(url, html) {
|
|
46
|
+
var _a, _b;
|
|
47
|
+
const text = String(html || '');
|
|
48
|
+
const type = ((_a = text.match(/comment_type\s*=\s*['"]([^'"]+)['"]/)) === null || _a === void 0 ? void 0 : _a[1]) || '';
|
|
49
|
+
const container = ((_b = text.match(/comment_container\s*=\s*['"]([^'"]+)['"]/)) === null || _b === void 0 ? void 0 : _b[1]) || '';
|
|
50
|
+
if (type && container)
|
|
51
|
+
return { type, container };
|
|
52
|
+
if (!text.includes('common-comment-block'))
|
|
53
|
+
return null;
|
|
54
|
+
return inferCommentContextFromUrl(url);
|
|
55
|
+
}
|
|
56
|
+
function buildCommentPageCandidates(pageUrl) {
|
|
57
|
+
const candidates = [pageUrl];
|
|
58
|
+
try {
|
|
59
|
+
const parsed = new URL(pageUrl);
|
|
60
|
+
const classMatch = parsed.pathname.match(/^\/class\/(\d+)(?:\.html)?\/?$/i);
|
|
61
|
+
if (classMatch)
|
|
62
|
+
candidates.push(`${constants_1.BASE_URL}/modpack/${classMatch[1]}.html`);
|
|
63
|
+
}
|
|
64
|
+
catch { }
|
|
65
|
+
return Array.from(new Set(candidates));
|
|
66
|
+
}
|
|
67
|
+
async function fetchCommentDocument(url, timeout) {
|
|
68
|
+
const firstUrl = (0, utils_1.fixUrl)(url);
|
|
69
|
+
if (!firstUrl)
|
|
70
|
+
throw new Error('MCMod 页面地址不能为空。');
|
|
71
|
+
let lastHtml = '';
|
|
72
|
+
for (const pageUrl of buildCommentPageCandidates(firstUrl)) {
|
|
73
|
+
const html = await (0, http_1.fetchMcmodText)(pageUrl, { headers: (0, http_1.getHeaders)(pageUrl) }, timeout);
|
|
74
|
+
lastHtml = html;
|
|
75
|
+
const context = extractCommentContext(pageUrl, html);
|
|
76
|
+
if (context)
|
|
77
|
+
return { pageUrl, html, context };
|
|
78
|
+
}
|
|
79
|
+
const fallback = extractCommentContext(firstUrl, lastHtml);
|
|
80
|
+
if (fallback)
|
|
81
|
+
return { pageUrl: firstUrl, html: lastHtml, context: fallback };
|
|
82
|
+
throw new Error('无法从页面解析评论上下文。');
|
|
24
83
|
}
|
|
25
84
|
function parseTarget(input) {
|
|
26
85
|
var _a, _b, _c;
|
|
@@ -69,11 +128,18 @@ function normalizeFloor(value) {
|
|
|
69
128
|
const match = String(value || '').match(/(\d+)/);
|
|
70
129
|
return match ? Number(match[1]) : null;
|
|
71
130
|
}
|
|
131
|
+
function cleanCommentFloor(value) {
|
|
132
|
+
const raw = String(value || '');
|
|
133
|
+
if (!raw)
|
|
134
|
+
return '';
|
|
135
|
+
const text = (0, utils_1.cleanText)(cheerio.load(`<div>${raw}</div>`)('div').text() || raw);
|
|
136
|
+
return text.replace(/\s+/g, ' ').trim();
|
|
137
|
+
}
|
|
72
138
|
function rowToComment(row) {
|
|
73
139
|
var _a, _b, _c, _d, _e, _f, _g;
|
|
74
140
|
return {
|
|
75
141
|
id: String((row === null || row === void 0 ? void 0 : row.id) || ''),
|
|
76
|
-
floor:
|
|
142
|
+
floor: cleanCommentFloor(row === null || row === void 0 ? void 0 : row.floor),
|
|
77
143
|
floorNo: normalizeFloor(row === null || row === void 0 ? void 0 : row.floor),
|
|
78
144
|
user: {
|
|
79
145
|
id: String(((_a = row === null || row === void 0 ? void 0 : row.user) === null || _a === void 0 ? void 0 : _a.id) || '0'),
|
|
@@ -161,20 +227,15 @@ async function fetchReplies(commentId, pageUrl, page, pageSize, timeout) {
|
|
|
161
227
|
};
|
|
162
228
|
}
|
|
163
229
|
async function fetchMcmodCommentThread(url, targetInput, page = 1, pageSize = 5, timeout = 15000) {
|
|
164
|
-
const pageUrl = (
|
|
165
|
-
if (!pageUrl)
|
|
166
|
-
throw new Error('MCMod 页面地址不能为空。');
|
|
167
|
-
const html = await (0, http_1.fetchMcmodText)(pageUrl, { headers: (0, http_1.getHeaders)(pageUrl) }, timeout);
|
|
168
|
-
const context = parseCommentContext(pageUrl, html);
|
|
230
|
+
const { pageUrl, html, context } = await fetchCommentDocument(url, timeout);
|
|
169
231
|
const target = parseTarget(targetInput);
|
|
170
232
|
const found = await findComment(context, pageUrl, target, timeout);
|
|
171
|
-
const replies =
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
context.container;
|
|
233
|
+
const replies = found.comment.replyCount > 0
|
|
234
|
+
? await fetchReplies(found.comment.id, pageUrl, Math.max(1, Number(page) || 1), pageSize, timeout)
|
|
235
|
+
: { replies: [], totalRows: 0, totalPages: 1 };
|
|
175
236
|
return {
|
|
176
237
|
pageUrl,
|
|
177
|
-
title,
|
|
238
|
+
title: readPageTitle(html, context),
|
|
178
239
|
context,
|
|
179
240
|
target,
|
|
180
241
|
main: found.comment,
|
|
@@ -187,6 +248,55 @@ async function fetchMcmodCommentThread(url, targetInput, page = 1, pageSize = 5,
|
|
|
187
248
|
replies: replies.replies,
|
|
188
249
|
};
|
|
189
250
|
}
|
|
251
|
+
async function fetchMcmodCommentList(url, page = 1, pageSize = 5, timeout = 15000) {
|
|
252
|
+
const listPage = Math.max(1, Number(page) || 1);
|
|
253
|
+
const listPageSize = Math.max(1, Math.floor(Number(pageSize) || 5));
|
|
254
|
+
const { pageUrl, html, context } = await fetchCommentDocument(url, timeout);
|
|
255
|
+
const firstData = await fetchCommentRows(context, pageUrl, 1, timeout);
|
|
256
|
+
const firstRows = Array.isArray(firstData.row) ? firstData.row : [];
|
|
257
|
+
const pageInfo = firstData.page || {};
|
|
258
|
+
const apiTotalRows = Number(pageInfo.total_row || 0) || 0;
|
|
259
|
+
const apiTotalPages = Number(pageInfo.total_page || pageInfo.end || (firstRows.length ? 1 : 0)) || (firstRows.length ? 1 : 0);
|
|
260
|
+
const apiPageSize = apiTotalRows && apiTotalPages
|
|
261
|
+
? Math.max(1, Math.ceil(apiTotalRows / apiTotalPages))
|
|
262
|
+
: Math.max(1, firstRows.length || listPageSize);
|
|
263
|
+
const pinnedRows = apiTotalRows && firstRows.length > apiPageSize ? firstRows.length - apiPageSize : 0;
|
|
264
|
+
const totalRows = apiTotalRows ? apiTotalRows + pinnedRows : firstRows.length;
|
|
265
|
+
const totalPages = Math.max(1, Math.ceil(totalRows / listPageSize));
|
|
266
|
+
const start = (listPage - 1) * listPageSize;
|
|
267
|
+
const comments = [];
|
|
268
|
+
if (start < totalRows) {
|
|
269
|
+
let collected = [];
|
|
270
|
+
if (start < firstRows.length) {
|
|
271
|
+
collected = firstRows.slice(start, Math.min(firstRows.length, start + listPageSize));
|
|
272
|
+
}
|
|
273
|
+
let globalIndex = start + collected.length;
|
|
274
|
+
let apiPage = Math.floor(Math.max(0, globalIndex - firstRows.length) / apiPageSize) + 2;
|
|
275
|
+
let offset = Math.max(0, globalIndex - firstRows.length) % apiPageSize;
|
|
276
|
+
while (collected.length < listPageSize && apiPage <= apiTotalPages) {
|
|
277
|
+
const data = await fetchCommentRows(context, pageUrl, apiPage, timeout);
|
|
278
|
+
const rows = Array.isArray(data.row) ? data.row : [];
|
|
279
|
+
if (!rows.length)
|
|
280
|
+
break;
|
|
281
|
+
const need = listPageSize - collected.length;
|
|
282
|
+
collected = collected.concat(rows.slice(offset, offset + need));
|
|
283
|
+
globalIndex = start + collected.length;
|
|
284
|
+
apiPage += 1;
|
|
285
|
+
offset = 0;
|
|
286
|
+
}
|
|
287
|
+
collected.forEach(row => comments.push(rowToComment(row)));
|
|
288
|
+
}
|
|
289
|
+
return {
|
|
290
|
+
pageUrl,
|
|
291
|
+
title: readPageTitle(html, context),
|
|
292
|
+
context,
|
|
293
|
+
page: listPage,
|
|
294
|
+
pageSize: listPageSize,
|
|
295
|
+
totalRows,
|
|
296
|
+
totalPages,
|
|
297
|
+
comments,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
190
300
|
function parseContentNodes(html, includeImages) {
|
|
191
301
|
var _a, _b;
|
|
192
302
|
const $ = cheerio.load(`<div class="root">${html || ''}</div>`, { decodeEntities: true });
|
|
@@ -225,6 +335,8 @@ function parseContentNodes(html, includeImages) {
|
|
|
225
335
|
buffer += alt;
|
|
226
336
|
else if (isEmotion)
|
|
227
337
|
buffer += ' [表情] ';
|
|
338
|
+
else
|
|
339
|
+
buffer += ' [图片] ';
|
|
228
340
|
return;
|
|
229
341
|
}
|
|
230
342
|
flush();
|
|
@@ -358,6 +470,139 @@ function badge(ctx, text, x, y, font, color = '#1f7a9d') {
|
|
|
358
470
|
ctx.fillText(text, x + 7, y + 4);
|
|
359
471
|
return w;
|
|
360
472
|
}
|
|
473
|
+
function measureBadge(ctx, text, font) {
|
|
474
|
+
ctx.font = `700 12px "${font}"`;
|
|
475
|
+
return ctx.measureText(text).width + 14;
|
|
476
|
+
}
|
|
477
|
+
function commentPreviewText(html, maxChars = 180) {
|
|
478
|
+
const nodes = parseContentNodes(html, false);
|
|
479
|
+
const text = nodes
|
|
480
|
+
.map(node => node.type === 'text' ? node.text : (node.alt ? `[图片: ${node.alt}]` : '[图片]'))
|
|
481
|
+
.join('\n')
|
|
482
|
+
.replace(/[ \t\f\v]+/g, ' ')
|
|
483
|
+
.replace(/\n{3,}/g, '\n\n')
|
|
484
|
+
.trim();
|
|
485
|
+
if (!text)
|
|
486
|
+
return '(无内容)';
|
|
487
|
+
return text.length > maxChars ? `${text.slice(0, maxChars).trim()}...` : text;
|
|
488
|
+
}
|
|
489
|
+
async function drawMcmodCommentList(list, options = {}) {
|
|
490
|
+
const font = rendering_1.GLOBAL_FONT_FAMILY;
|
|
491
|
+
const width = 900;
|
|
492
|
+
const margin = 28;
|
|
493
|
+
const contentW = width - margin * 2;
|
|
494
|
+
const bodyW = contentW - 124;
|
|
495
|
+
const dummyCanvas = (0, rendering_1.createCanvas)(100, 100);
|
|
496
|
+
const dummy = dummyCanvas.getContext('2d');
|
|
497
|
+
for (const comment of list.comments) {
|
|
498
|
+
comment._avatar = await loadAvatar(comment.user.avatar, list.pageUrl);
|
|
499
|
+
comment._preview = commentPreviewText(comment.content, options.previewChars || 180);
|
|
500
|
+
}
|
|
501
|
+
const headerH = 112;
|
|
502
|
+
const listHeaderH = 58;
|
|
503
|
+
const itemHeights = list.comments.map(comment => {
|
|
504
|
+
dummy.font = `15px "${font}"`;
|
|
505
|
+
const previewH = (0, rendering_1.wrapText)(dummy, comment._preview, 0, 0, bodyW, 23, 4, false);
|
|
506
|
+
return Math.max(118, 84 + previewH);
|
|
507
|
+
});
|
|
508
|
+
const emptyH = list.comments.length ? 0 : 68;
|
|
509
|
+
const itemsH = itemHeights.reduce((sum, h) => sum + h + 12, 0);
|
|
510
|
+
const totalH = margin + headerH + 18 + listHeaderH + itemsH + emptyH + 42;
|
|
511
|
+
const canvas = (0, rendering_1.createCanvas)(width, totalH);
|
|
512
|
+
const ctx = canvas.getContext('2d');
|
|
513
|
+
ctx.textBaseline = 'top';
|
|
514
|
+
ctx.fillStyle = '#eef4f7';
|
|
515
|
+
ctx.fillRect(0, 0, width, totalH);
|
|
516
|
+
ctx.fillStyle = '#0f5d7a';
|
|
517
|
+
(0, rendering_1.roundRect)(ctx, margin, margin, contentW, headerH, 14);
|
|
518
|
+
ctx.fill();
|
|
519
|
+
ctx.fillStyle = '#2f9ab7';
|
|
520
|
+
(0, rendering_1.roundRect)(ctx, margin + 18, margin + 18, 78, 78, 12);
|
|
521
|
+
ctx.fill();
|
|
522
|
+
ctx.fillStyle = '#ffffff';
|
|
523
|
+
ctx.font = `800 30px "${font}"`;
|
|
524
|
+
ctx.textAlign = 'center';
|
|
525
|
+
ctx.fillText('MC', margin + 57, margin + 32);
|
|
526
|
+
ctx.font = `700 13px "${font}"`;
|
|
527
|
+
ctx.fillText('MOD', margin + 57, margin + 66);
|
|
528
|
+
ctx.textAlign = 'left';
|
|
529
|
+
ctx.fillStyle = '#fff';
|
|
530
|
+
ctx.font = `800 28px "${font}"`;
|
|
531
|
+
(0, rendering_1.wrapText)(ctx, list.title || 'MCMod 评论列表', margin + 116, margin + 20, contentW - 146, 34, 2, true);
|
|
532
|
+
ctx.font = `600 14px "${font}"`;
|
|
533
|
+
ctx.fillStyle = 'rgba(255,255,255,0.84)';
|
|
534
|
+
ctx.fillText(`mcmod.cn / ${list.context.type}:${list.context.container}`, margin + 116, margin + 82);
|
|
535
|
+
let y = margin + headerH + 18;
|
|
536
|
+
ctx.fillStyle = '#153743';
|
|
537
|
+
ctx.font = `800 20px "${font}"`;
|
|
538
|
+
ctx.fillText(`主评论 ${list.page}/${list.totalPages}`, margin + 4, y + 4);
|
|
539
|
+
ctx.fillStyle = '#607d8b';
|
|
540
|
+
ctx.font = `13px "${font}"`;
|
|
541
|
+
ctx.fillText(`每页 ${list.pageSize} 条,共 ${list.totalRows} 条;输入 n/p 翻页,q 退出`, margin + 4, y + 32);
|
|
542
|
+
y += listHeaderH;
|
|
543
|
+
if (!list.comments.length) {
|
|
544
|
+
ctx.fillStyle = '#ffffff';
|
|
545
|
+
(0, rendering_1.roundRect)(ctx, margin, y, contentW, 56, 10);
|
|
546
|
+
ctx.fill();
|
|
547
|
+
ctx.strokeStyle = '#dceaf0';
|
|
548
|
+
ctx.lineWidth = 1;
|
|
549
|
+
(0, rendering_1.roundRect)(ctx, margin, y, contentW, 56, 10);
|
|
550
|
+
ctx.stroke();
|
|
551
|
+
ctx.fillStyle = '#78909c';
|
|
552
|
+
ctx.font = `14px "${font}"`;
|
|
553
|
+
ctx.fillText('本页没有评论。', margin + 18, y + 18);
|
|
554
|
+
y += emptyH;
|
|
555
|
+
}
|
|
556
|
+
for (let i = 0; i < list.comments.length; i++) {
|
|
557
|
+
const comment = list.comments[i];
|
|
558
|
+
const cardH = itemHeights[i];
|
|
559
|
+
ctx.fillStyle = '#ffffff';
|
|
560
|
+
(0, rendering_1.roundRect)(ctx, margin, y, contentW, cardH, 10);
|
|
561
|
+
ctx.fill();
|
|
562
|
+
ctx.strokeStyle = '#dceaf0';
|
|
563
|
+
ctx.lineWidth = 1;
|
|
564
|
+
(0, rendering_1.roundRect)(ctx, margin, y, contentW, cardH, 10);
|
|
565
|
+
ctx.stroke();
|
|
566
|
+
drawAvatar(ctx, comment._avatar, margin + 22, y + 22, 58, comment.user.name, font);
|
|
567
|
+
let tx = margin + 98;
|
|
568
|
+
let ty = y + 18;
|
|
569
|
+
let right = width - margin - 18;
|
|
570
|
+
const replyText = `回复 ${comment.replyCount}`;
|
|
571
|
+
if (comment.replyCount > 0) {
|
|
572
|
+
const w = measureBadge(ctx, replyText, font);
|
|
573
|
+
badge(ctx, replyText, right - w, ty + 1, font, '#607d8b');
|
|
574
|
+
right -= w + 8;
|
|
575
|
+
}
|
|
576
|
+
if (comment.user.level !== undefined) {
|
|
577
|
+
const levelText = `Lv.${comment.user.level}`;
|
|
578
|
+
const w = measureBadge(ctx, levelText, font);
|
|
579
|
+
badge(ctx, levelText, right - w, ty + 1, font, '#1f7a9d');
|
|
580
|
+
right -= w + 8;
|
|
581
|
+
}
|
|
582
|
+
if (comment.floor) {
|
|
583
|
+
const w = measureBadge(ctx, comment.floor, font);
|
|
584
|
+
badge(ctx, comment.floor, right - w, ty + 1, font, '#d2691e');
|
|
585
|
+
right -= w + 8;
|
|
586
|
+
}
|
|
587
|
+
ctx.fillStyle = '#102a35';
|
|
588
|
+
ctx.font = `800 18px "${font}"`;
|
|
589
|
+
await (0, rendering_1.drawTextWithTwemoji)(ctx, comment.user.name, tx, ty, Math.max(80, right - tx), 23, 1, true);
|
|
590
|
+
ty += 28;
|
|
591
|
+
ctx.fillStyle = '#78909c';
|
|
592
|
+
ctx.font = `13px "${font}"`;
|
|
593
|
+
ctx.fillText(`${comment.time.source || '--'} ${comment.time.range || ''} ID:${comment.id}`, tx, ty);
|
|
594
|
+
ty += 30;
|
|
595
|
+
ctx.fillStyle = '#263238';
|
|
596
|
+
ctx.font = `15px "${font}"`;
|
|
597
|
+
await (0, rendering_1.drawTextWithTwemoji)(ctx, comment._preview, tx, ty, bodyW, 23, 4, true);
|
|
598
|
+
y += cardH + 12;
|
|
599
|
+
}
|
|
600
|
+
ctx.fillStyle = '#78909c';
|
|
601
|
+
ctx.font = `12px "${font}"`;
|
|
602
|
+
ctx.textAlign = 'center';
|
|
603
|
+
ctx.fillText('Generated by Koishi | Powered by MCMod', width / 2, totalH - 24);
|
|
604
|
+
return await canvas.encode('png');
|
|
605
|
+
}
|
|
361
606
|
async function drawMcmodCommentThread(thread, options = {}) {
|
|
362
607
|
var _a;
|
|
363
608
|
const includeImages = options.includeImages !== false;
|
package/dist/mcmod/cards.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.fetchMcmodCommentThread = exports.drawMcmodCommentThread = exports.createInfoCard = exports.drawCenterCardImpl = exports.drawCenterCard = exports.drawAuthorCard = exports.drawTutorialCard = exports.drawModCard = void 0;
|
|
3
|
+
exports.fetchMcmodCommentThread = exports.fetchMcmodCommentList = exports.drawMcmodCommentThread = exports.drawMcmodCommentList = exports.createInfoCard = exports.drawCenterCardImpl = exports.drawCenterCard = exports.drawAuthorCard = exports.drawTutorialCard = exports.drawModCard = void 0;
|
|
4
4
|
var mod_card_1 = require("./cards/mod-card");
|
|
5
5
|
Object.defineProperty(exports, "drawModCard", { enumerable: true, get: function () { return mod_card_1.drawModCard; } });
|
|
6
6
|
var tutorial_card_1 = require("./cards/tutorial-card");
|
|
@@ -13,5 +13,7 @@ Object.defineProperty(exports, "drawCenterCardImpl", { enumerable: true, get: fu
|
|
|
13
13
|
var info_card_1 = require("./cards/info-card");
|
|
14
14
|
Object.defineProperty(exports, "createInfoCard", { enumerable: true, get: function () { return info_card_1.createInfoCard; } });
|
|
15
15
|
var comment_card_1 = require("./cards/comment-card");
|
|
16
|
+
Object.defineProperty(exports, "drawMcmodCommentList", { enumerable: true, get: function () { return comment_card_1.drawMcmodCommentList; } });
|
|
16
17
|
Object.defineProperty(exports, "drawMcmodCommentThread", { enumerable: true, get: function () { return comment_card_1.drawMcmodCommentThread; } });
|
|
18
|
+
Object.defineProperty(exports, "fetchMcmodCommentList", { enumerable: true, get: function () { return comment_card_1.fetchMcmodCommentList; } });
|
|
17
19
|
Object.defineProperty(exports, "fetchMcmodCommentThread", { enumerable: true, get: function () { return comment_card_1.fetchMcmodCommentThread; } });
|
package/dist/mcmod/plugin.js
CHANGED
|
@@ -9,9 +9,11 @@ const http_1 = require("./http");
|
|
|
9
9
|
const rendering_1 = require("./rendering");
|
|
10
10
|
const search_1 = require("./search");
|
|
11
11
|
const utils_1 = require("./utils");
|
|
12
|
+
const match_1 = require("../match");
|
|
12
13
|
// ================= 状态管理和常量 =================
|
|
13
14
|
const searchStates = new Map();
|
|
14
15
|
const commentStates = new Map();
|
|
16
|
+
const commentListStates = new Map();
|
|
15
17
|
// ================= Koishi =================
|
|
16
18
|
exports.name = 'mcmod-search';
|
|
17
19
|
exports.Config = Schema.object({
|
|
@@ -67,6 +69,12 @@ function apply(ctx, config) {
|
|
|
67
69
|
clearTimeout(state.timer);
|
|
68
70
|
commentStates.delete(cid);
|
|
69
71
|
}
|
|
72
|
+
function clearCommentListState(cid) {
|
|
73
|
+
const state = commentListStates.get(cid);
|
|
74
|
+
if (state && state.timer)
|
|
75
|
+
clearTimeout(state.timer);
|
|
76
|
+
commentListStates.delete(cid);
|
|
77
|
+
}
|
|
70
78
|
// --- 排队系统 ---
|
|
71
79
|
const queue = [];
|
|
72
80
|
let isProcessing = false;
|
|
@@ -166,6 +174,80 @@ function apply(ctx, config) {
|
|
|
166
174
|
commentStates.set(session.cid, nextState);
|
|
167
175
|
return nextState;
|
|
168
176
|
}
|
|
177
|
+
async function renderCommentListPage(session, state, nextPage) {
|
|
178
|
+
const page = Math.max(1, Number(nextPage) || 1);
|
|
179
|
+
const list = await (0, cards_1.fetchMcmodCommentList)(state.url, page, state.pageSize, config.requestTimeout || 15000);
|
|
180
|
+
if (page > list.totalPages) {
|
|
181
|
+
await session.send('没有更多页面了。');
|
|
182
|
+
return state;
|
|
183
|
+
}
|
|
184
|
+
const img = await (0, cards_1.drawMcmodCommentList)(list);
|
|
185
|
+
await tryWithdraw(session, state.messageIds);
|
|
186
|
+
const sentMessageIds = await session.send(h.image(await (0, utils_1.toImageSrc)(img)));
|
|
187
|
+
const timer = setTimeout(() => commentListStates.delete(session.cid), config.timeouts || constants_1.TIMEOUT_MS);
|
|
188
|
+
if (state.timer)
|
|
189
|
+
clearTimeout(state.timer);
|
|
190
|
+
const nextState = {
|
|
191
|
+
...state,
|
|
192
|
+
page: list.page,
|
|
193
|
+
totalPages: list.totalPages,
|
|
194
|
+
messageIds: normalizeMessageIds(sentMessageIds),
|
|
195
|
+
timer,
|
|
196
|
+
};
|
|
197
|
+
commentListStates.set(session.cid, nextState);
|
|
198
|
+
return nextState;
|
|
199
|
+
}
|
|
200
|
+
async function renderSearchItem(session, item, type) {
|
|
201
|
+
var _a;
|
|
202
|
+
await (0, http_1.ensureValidCookie)();
|
|
203
|
+
let img;
|
|
204
|
+
if (type === 'author')
|
|
205
|
+
img = await (0, cards_1.drawAuthorCard)(item.link);
|
|
206
|
+
else if (type === 'user') {
|
|
207
|
+
const uid = ((_a = item.link.match(/\/(\d+)(?:\.html|\/)?$/)) === null || _a === void 0 ? void 0 : _a[1]) || '0';
|
|
208
|
+
img = await (0, cards_1.drawCenterCardImpl)(uid, logger);
|
|
209
|
+
}
|
|
210
|
+
else if (type === 'mod' || type === 'pack')
|
|
211
|
+
img = await (0, cards_1.drawModCard)(item.link);
|
|
212
|
+
else if (type === 'tutorial')
|
|
213
|
+
img = await (0, cards_1.drawTutorialCard)(item.link);
|
|
214
|
+
else
|
|
215
|
+
img = await (0, cards_1.createInfoCard)(item.link, type);
|
|
216
|
+
await session.send(h.image(await (0, utils_1.toImageSrc)(img)));
|
|
217
|
+
if (config.sendLink)
|
|
218
|
+
await session.send(`链接: ${item.link}`);
|
|
219
|
+
}
|
|
220
|
+
async function renderDirectSearchTarget(session, item, type, options = {}) {
|
|
221
|
+
clearState(session.cid);
|
|
222
|
+
clearCommentState(session.cid);
|
|
223
|
+
clearCommentListState(session.cid);
|
|
224
|
+
if (options === null || options === void 0 ? void 0 : options.commentTarget) {
|
|
225
|
+
const state = {
|
|
226
|
+
url: item.link,
|
|
227
|
+
target: String(options.commentTarget),
|
|
228
|
+
pageSize: getCommentPageSize(options === null || options === void 0 ? void 0 : options.size),
|
|
229
|
+
page: 1,
|
|
230
|
+
totalPages: 1,
|
|
231
|
+
messageIds: [],
|
|
232
|
+
timer: null,
|
|
233
|
+
};
|
|
234
|
+
await renderCommentPage(session, state, state.page);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
if (options === null || options === void 0 ? void 0 : options.comments) {
|
|
238
|
+
const state = {
|
|
239
|
+
url: item.link,
|
|
240
|
+
pageSize: getCommentPageSize(options === null || options === void 0 ? void 0 : options.size),
|
|
241
|
+
page: 1,
|
|
242
|
+
totalPages: 1,
|
|
243
|
+
messageIds: [],
|
|
244
|
+
timer: null,
|
|
245
|
+
};
|
|
246
|
+
await renderCommentListPage(session, state, state.page);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
await renderSearchItem(session, item, type);
|
|
250
|
+
}
|
|
169
251
|
// --- 注册指令 ---
|
|
170
252
|
const prefix = ((_a = config === null || config === void 0 ? void 0 : config.prefixes) === null || _a === void 0 ? void 0 : _a.cnmc) || 'cnmc';
|
|
171
253
|
const commandTypes = ['mod', 'data', 'pack', 'tutorial', 'author', 'user'];
|
|
@@ -173,6 +255,8 @@ function apply(ctx, config) {
|
|
|
173
255
|
`${prefix} <关键词> | 默认搜索 Mod`,
|
|
174
256
|
`${prefix}.mod/.data/.pack/.tutorial/.author/.user <关键词>`,
|
|
175
257
|
`${prefix}.comment <url> <楼层|id:评论ID> [page] | 渲染评论与子评论`,
|
|
258
|
+
`${prefix}.comments <url> [page] | 渲染页面主评论列表`,
|
|
259
|
+
'选项:--direct 精确命中时直接出图,--comments 精确命中时渲染评论列表',
|
|
176
260
|
'列表交互:输入序号查看,n 下一页,p 上一页,q 退出',
|
|
177
261
|
].join('\n'));
|
|
178
262
|
ctx.command(`${prefix}.comment <url:string> <target:string> [page:number]`, '渲染 MCMod 评论与子评论')
|
|
@@ -185,6 +269,7 @@ function apply(ctx, config) {
|
|
|
185
269
|
await (0, http_1.ensureValidCookie)();
|
|
186
270
|
clearState(session.cid);
|
|
187
271
|
clearCommentState(session.cid);
|
|
272
|
+
clearCommentListState(session.cid);
|
|
188
273
|
const state = {
|
|
189
274
|
url,
|
|
190
275
|
target,
|
|
@@ -202,42 +287,64 @@ function apply(ctx, config) {
|
|
|
202
287
|
}
|
|
203
288
|
});
|
|
204
289
|
});
|
|
290
|
+
ctx.command(`${prefix}.comments <url:string> [page:number]`, '渲染 MCMod 页面主评论列表')
|
|
291
|
+
.option('size', '-s, --size <size:number> 主评论单页数量')
|
|
292
|
+
.action(async ({ session, options }, url, page) => {
|
|
293
|
+
if (!url)
|
|
294
|
+
return `用法:${prefix}.comments <url> [page] [-s 数量]`;
|
|
295
|
+
enqueue(session, 'comment-list-render', async () => {
|
|
296
|
+
try {
|
|
297
|
+
await (0, http_1.ensureValidCookie)();
|
|
298
|
+
clearState(session.cid);
|
|
299
|
+
clearCommentState(session.cid);
|
|
300
|
+
clearCommentListState(session.cid);
|
|
301
|
+
const state = {
|
|
302
|
+
url,
|
|
303
|
+
pageSize: getCommentPageSize(options === null || options === void 0 ? void 0 : options.size),
|
|
304
|
+
page: Math.max(1, Number(page) || 1),
|
|
305
|
+
totalPages: 1,
|
|
306
|
+
messageIds: [],
|
|
307
|
+
timer: null,
|
|
308
|
+
};
|
|
309
|
+
await renderCommentListPage(session, state, state.page);
|
|
310
|
+
}
|
|
311
|
+
catch (e) {
|
|
312
|
+
logger.error(e);
|
|
313
|
+
await session.send(`评论列表渲染失败: ${e.message}`);
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
});
|
|
205
317
|
commandTypes.forEach(type => {
|
|
206
318
|
ctx.command(`${prefix}.${type} <keyword:text>`)
|
|
207
|
-
.
|
|
319
|
+
.option('direct', '-d, --direct 精确命中时直接渲染')
|
|
320
|
+
.option('comments', '--comments 精确命中时渲染主评论列表')
|
|
321
|
+
.option('commentTarget', '--comment-target <target:string> 精确命中时渲染指定楼层或评论 ID')
|
|
322
|
+
.option('size', '-s, --size <size:number> 评论单页数量')
|
|
323
|
+
.action(async ({ session, options }, keyword) => {
|
|
208
324
|
if (!keyword)
|
|
209
325
|
return '请输入关键词。';
|
|
210
326
|
// 将搜索任务加入队列
|
|
211
327
|
enqueue(session, `search-${type}`, async () => {
|
|
212
|
-
var _a;
|
|
213
328
|
try {
|
|
214
329
|
if (config.debug)
|
|
215
330
|
logger.debug(`[${session.userId}] 正在搜索 ${keyword} ...`);
|
|
216
|
-
|
|
331
|
+
const directMode = !!((options === null || options === void 0 ? void 0 : options.direct) || (options === null || options === void 0 ? void 0 : options.comments) || (options === null || options === void 0 ? void 0 : options.commentTarget));
|
|
332
|
+
const searchResult = directMode
|
|
333
|
+
? await (0, search_1.fetchDirectSearch)(keyword, type)
|
|
334
|
+
: { results: await (0, search_1.fetchSearch)(keyword, type), direct: null };
|
|
335
|
+
let results = searchResult.results;
|
|
217
336
|
if (!results.length) {
|
|
218
337
|
await session.send('未找到相关结果。(备用也没用,我劝你换个关键词试试)');
|
|
219
338
|
return;
|
|
220
339
|
}
|
|
340
|
+
const directItem = searchResult.direct || (directMode ? (0, match_1.selectExactSearchResult)(results, keyword) : null);
|
|
341
|
+
if (directItem) {
|
|
342
|
+
await renderDirectSearchTarget(session, directItem, type, options);
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
221
345
|
// 单结果直接处理
|
|
222
346
|
if (results.length === 1) {
|
|
223
|
-
|
|
224
|
-
await (0, http_1.ensureValidCookie)();
|
|
225
|
-
let img;
|
|
226
|
-
if (type === 'author')
|
|
227
|
-
img = await (0, cards_1.drawAuthorCard)(item.link);
|
|
228
|
-
else if (type === 'user') {
|
|
229
|
-
const uid = ((_a = item.link.match(/\/(\d+)(?:\.html|\/)?$/)) === null || _a === void 0 ? void 0 : _a[1]) || '0';
|
|
230
|
-
img = await (0, cards_1.drawCenterCardImpl)(uid, logger);
|
|
231
|
-
}
|
|
232
|
-
else if (type === 'mod' || type === 'pack')
|
|
233
|
-
img = await (0, cards_1.drawModCard)(item.link);
|
|
234
|
-
else if (type === 'tutorial')
|
|
235
|
-
img = await (0, cards_1.drawTutorialCard)(item.link);
|
|
236
|
-
else
|
|
237
|
-
img = await (0, cards_1.createInfoCard)(item.link, type);
|
|
238
|
-
await session.send(h.image(await (0, utils_1.toImageSrc)(img)));
|
|
239
|
-
if (config.sendLink)
|
|
240
|
-
await session.send(`链接: ${item.link}`);
|
|
347
|
+
await renderDirectSearchTarget(session, results[0], type, options);
|
|
241
348
|
return;
|
|
242
349
|
}
|
|
243
350
|
// 多结果:初始化状态(隔离在 session.cid)
|
|
@@ -260,25 +367,33 @@ function apply(ctx, config) {
|
|
|
260
367
|
});
|
|
261
368
|
});
|
|
262
369
|
ctx.command(`${prefix} <keyword:text>`)
|
|
263
|
-
.
|
|
370
|
+
.option('direct', '-d, --direct 精确命中时直接渲染')
|
|
371
|
+
.option('comments', '--comments 精确命中时渲染主评论列表')
|
|
372
|
+
.option('commentTarget', '--comment-target <target:string> 精确命中时渲染指定楼层或评论 ID')
|
|
373
|
+
.option('size', '-s, --size <size:number> 评论单页数量')
|
|
374
|
+
.action(async ({ session, options }, keyword) => {
|
|
264
375
|
if (!keyword)
|
|
265
376
|
return '请输入关键词。';
|
|
266
377
|
enqueue(session, 'search-mod', async () => {
|
|
267
378
|
try {
|
|
268
379
|
if (config.debug)
|
|
269
380
|
logger.debug(`[${session.userId}] 正在搜索 ${keyword} ...`);
|
|
270
|
-
const
|
|
381
|
+
const directMode = !!((options === null || options === void 0 ? void 0 : options.direct) || (options === null || options === void 0 ? void 0 : options.comments) || (options === null || options === void 0 ? void 0 : options.commentTarget));
|
|
382
|
+
const searchResult = directMode
|
|
383
|
+
? await (0, search_1.fetchDirectSearch)(keyword, 'mod')
|
|
384
|
+
: { results: await (0, search_1.fetchSearch)(keyword, 'mod'), direct: null };
|
|
385
|
+
const results = searchResult.results;
|
|
271
386
|
if (!results.length) {
|
|
272
387
|
await session.send('未找到相关结果。(备用也没用,我劝你换个关键词试试)');
|
|
273
388
|
return;
|
|
274
389
|
}
|
|
390
|
+
const directItem = searchResult.direct || (directMode ? (0, match_1.selectExactSearchResult)(results, keyword) : null);
|
|
391
|
+
if (directItem) {
|
|
392
|
+
await renderDirectSearchTarget(session, directItem, 'mod', options);
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
275
395
|
if (results.length === 1) {
|
|
276
|
-
|
|
277
|
-
await (0, http_1.ensureValidCookie)();
|
|
278
|
-
const img = await (0, cards_1.drawModCard)(item.link);
|
|
279
|
-
await session.send(h.image(await (0, utils_1.toImageSrc)(img)));
|
|
280
|
-
if (config.sendLink)
|
|
281
|
-
await session.send(`链接: ${item.link}`);
|
|
396
|
+
await renderDirectSearchTarget(session, results[0], 'mod', options);
|
|
282
397
|
return;
|
|
283
398
|
}
|
|
284
399
|
clearState(session.cid);
|
|
@@ -301,6 +416,42 @@ function apply(ctx, config) {
|
|
|
301
416
|
}
|
|
302
417
|
});
|
|
303
418
|
});
|
|
419
|
+
// --- 中间件 (处理评论列表翻页) ---
|
|
420
|
+
ctx.middleware(async (session, next) => {
|
|
421
|
+
const state = commentListStates.get(session.cid);
|
|
422
|
+
if (!state)
|
|
423
|
+
return next();
|
|
424
|
+
const input = session.content.trim().toLowerCase();
|
|
425
|
+
if (input === 'q' || input === '退出') {
|
|
426
|
+
clearCommentListState(session.cid);
|
|
427
|
+
await tryWithdraw(session, state.messageIds);
|
|
428
|
+
await session.send('已退出评论列表翻页。');
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
if (input === 'p' || input === 'n') {
|
|
432
|
+
enqueue(session, 'comment-list-page-turn', async () => {
|
|
433
|
+
const currentState = commentListStates.get(session.cid);
|
|
434
|
+
if (!currentState)
|
|
435
|
+
return;
|
|
436
|
+
const nextPage = input === 'n'
|
|
437
|
+
? Number(currentState.page || 1) + 1
|
|
438
|
+
: Number(currentState.page || 1) - 1;
|
|
439
|
+
if (nextPage < 1 || nextPage > Number(currentState.totalPages || 1)) {
|
|
440
|
+
await session.send('没有更多页面了。');
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
await renderCommentListPage(session, currentState, nextPage);
|
|
445
|
+
}
|
|
446
|
+
catch (e) {
|
|
447
|
+
logger.error(e);
|
|
448
|
+
await session.send(`评论列表翻页失败: ${e.message}`);
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
return next();
|
|
454
|
+
});
|
|
304
455
|
// --- 中间件 (处理评论翻页) ---
|
|
305
456
|
ctx.middleware(async (session, next) => {
|
|
306
457
|
const state = commentStates.get(session.cid);
|
package/dist/mcmod/search.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.fetchSearch = fetchSearch;
|
|
4
|
+
exports.fetchDirectSearch = fetchDirectSearch;
|
|
4
5
|
exports.fetchSearchFallback = fetchSearchFallback;
|
|
5
6
|
exports.formatListPage = formatListPage;
|
|
6
7
|
const constants_1 = require("./constants");
|
|
7
8
|
const http_1 = require("./http");
|
|
8
9
|
const utils_1 = require("./utils");
|
|
10
|
+
const match_1 = require("../match");
|
|
9
11
|
const cheerio = require('cheerio');
|
|
10
12
|
async function fetchSearch(query, typeKey) {
|
|
11
13
|
const filterMap = { mod: 1, pack: 2, data: 3, tutorial: 4, author: 5, user: 6 };
|
|
@@ -55,6 +57,53 @@ async function fetchSearch(query, typeKey) {
|
|
|
55
57
|
}
|
|
56
58
|
return results;
|
|
57
59
|
}
|
|
60
|
+
async function expandDirectQueryFromModrinth(query, typeKey) {
|
|
61
|
+
if (!['mod', 'pack'].includes(typeKey))
|
|
62
|
+
return [];
|
|
63
|
+
const raw = String(query || '').trim();
|
|
64
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9_-]{1,64}$/.test(raw))
|
|
65
|
+
return [];
|
|
66
|
+
try {
|
|
67
|
+
const res = await (0, http_1.fetchWithTimeout)(`https://api.modrinth.com/v2/project/${encodeURIComponent(raw)}`, {
|
|
68
|
+
headers: {
|
|
69
|
+
'User-Agent': 'koishi-plugin-cfmrmod',
|
|
70
|
+
'Accept': 'application/json',
|
|
71
|
+
},
|
|
72
|
+
}, 8000);
|
|
73
|
+
if (!res.ok)
|
|
74
|
+
return [];
|
|
75
|
+
const project = await res.json();
|
|
76
|
+
const title = (0, utils_1.cleanText)((project === null || project === void 0 ? void 0 : project.title) || '');
|
|
77
|
+
const slug = (0, utils_1.cleanText)((project === null || project === void 0 ? void 0 : project.slug) || '');
|
|
78
|
+
const aliases = [
|
|
79
|
+
title.replace(/\s*[\((][^\))]+[\))]\s*/g, ' ').trim(),
|
|
80
|
+
title,
|
|
81
|
+
slug,
|
|
82
|
+
].filter(Boolean);
|
|
83
|
+
return Array.from(new Set(aliases));
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
async function fetchDirectSearch(query, typeKey) {
|
|
90
|
+
const results = await fetchSearch(query, typeKey);
|
|
91
|
+
const direct = (0, match_1.selectExactSearchResult)(results, query);
|
|
92
|
+
if (direct)
|
|
93
|
+
return { results, direct, query };
|
|
94
|
+
const aliases = await expandDirectQueryFromModrinth(query, typeKey);
|
|
95
|
+
for (const alias of aliases) {
|
|
96
|
+
if ((0, match_1.normalizeSearchText)(alias) === (0, match_1.normalizeSearchText)(query))
|
|
97
|
+
continue;
|
|
98
|
+
const expandedResults = await fetchSearch(alias, typeKey);
|
|
99
|
+
const expandedDirect = (0, match_1.selectExactSearchResult)(expandedResults, alias, 650) ||
|
|
100
|
+
(expandedResults.length === 1 ? expandedResults[0] : null);
|
|
101
|
+
if (expandedDirect) {
|
|
102
|
+
return { results: expandedResults, direct: expandedDirect, query: alias };
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return { results, direct: null, query };
|
|
106
|
+
}
|
|
58
107
|
async function fetchSearchFallback(query, typeKey) {
|
|
59
108
|
const apiType = constants_1.FALLBACK_TYPE_MAP[typeKey];
|
|
60
109
|
if (!apiType)
|
|
@@ -88,6 +137,12 @@ async function fetchSearchFallback(query, typeKey) {
|
|
|
88
137
|
summary = (0, utils_1.cleanText)($el.find('i').text());
|
|
89
138
|
link = `https://www.mcmod.cn/author/${id}.html`;
|
|
90
139
|
}
|
|
140
|
+
else if (typeKey === 'pack') {
|
|
141
|
+
const rawText = (0, utils_1.cleanText)($el.text());
|
|
142
|
+
title = rawText.replace(/^ID:\d+\s*/, '');
|
|
143
|
+
link = `https://www.mcmod.cn/modpack/${id}.html`;
|
|
144
|
+
summary = `ID: ${id}`;
|
|
145
|
+
}
|
|
91
146
|
else {
|
|
92
147
|
const rawText = (0, utils_1.cleanText)($el.text());
|
|
93
148
|
title = rawText.replace(/^ID:\d+\s*/, '');
|
package/dist/nlu.js
CHANGED
|
@@ -37,6 +37,8 @@ const PLATFORM_TYPES = {
|
|
|
37
37
|
cf: new Set(['mod', 'pack', 'resource', 'shader', 'plugin']),
|
|
38
38
|
mr: new Set(['mod', 'pack', 'resource', 'shader', 'plugin']),
|
|
39
39
|
};
|
|
40
|
+
const COMMENT_ACTIONS = new Set(['comment', 'comment_thread', 'thread_comment', 'floor_comment', 'reply_comment']);
|
|
41
|
+
const COMMENT_LIST_ACTIONS = new Set(['comments', 'comment_list', 'all_comments', 'comments_list']);
|
|
40
42
|
function normalizeEndpoint(endpoint) {
|
|
41
43
|
const value = String(endpoint || DEFAULT_ENDPOINT).trim().replace(/\/+$/, '');
|
|
42
44
|
if (/\/chat\/completions$/i.test(value))
|
|
@@ -72,15 +74,42 @@ function normalizeType(value, platform) {
|
|
|
72
74
|
return ((_a = PLATFORM_TYPES[platform]) === null || _a === void 0 ? void 0 : _a.has(type)) ? type : 'mod';
|
|
73
75
|
}
|
|
74
76
|
function normalizeAiDecision(raw) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
+
const rawAction = String((raw === null || raw === void 0 ? void 0 : raw.action) || '').trim().toLowerCase();
|
|
78
|
+
const action = COMMENT_ACTIONS.has(rawAction)
|
|
79
|
+
? 'comment_thread'
|
|
80
|
+
: COMMENT_LIST_ACTIONS.has(rawAction)
|
|
81
|
+
? 'comment_list'
|
|
82
|
+
: (rawAction === 'direct_search' || rawAction === 'open' || rawAction === 'render')
|
|
83
|
+
? 'search'
|
|
84
|
+
: rawAction;
|
|
85
|
+
if (action && !['search', 'comment_thread', 'comment_list'].includes(action))
|
|
77
86
|
return { action: 'ignore' };
|
|
87
|
+
const platform = normalizePlatform(raw === null || raw === void 0 ? void 0 : raw.platform);
|
|
88
|
+
const type = normalizeType(raw === null || raw === void 0 ? void 0 : raw.type, platform);
|
|
89
|
+
const url = String((raw === null || raw === void 0 ? void 0 : raw.url) || (raw === null || raw === void 0 ? void 0 : raw.link) || '').replace(/[\r\n]+/g, ' ').trim();
|
|
90
|
+
const target = String((raw === null || raw === void 0 ? void 0 : raw.target) || (raw === null || raw === void 0 ? void 0 : raw.floor) || (raw === null || raw === void 0 ? void 0 : raw.commentId) || (raw === null || raw === void 0 ? void 0 : raw.comment_id) || '').replace(/[\r\n]+/g, ' ').trim();
|
|
91
|
+
const page = Math.max(1, Number((raw === null || raw === void 0 ? void 0 : raw.page) || 1) || 1);
|
|
92
|
+
const size = Math.max(0, Number((raw === null || raw === void 0 ? void 0 : raw.size) || (raw === null || raw === void 0 ? void 0 : raw.pageSize) || (raw === null || raw === void 0 ? void 0 : raw.page_size) || 0) || 0);
|
|
78
93
|
const query = String((raw === null || raw === void 0 ? void 0 : raw.query) || (raw === null || raw === void 0 ? void 0 : raw.keyword) || '').replace(/[\r\n]+/g, ' ').trim();
|
|
94
|
+
if (action === 'comment_thread') {
|
|
95
|
+
if (!target)
|
|
96
|
+
return { action: 'ignore' };
|
|
97
|
+
if (url)
|
|
98
|
+
return { action: 'comment_thread', platform: 'mcmod', type, url, target, page, size };
|
|
99
|
+
if (query)
|
|
100
|
+
return { action: 'comment_thread', platform: 'mcmod', type, query, target, page, size };
|
|
101
|
+
return { action: 'ignore' };
|
|
102
|
+
}
|
|
103
|
+
if (action === 'comment_list') {
|
|
104
|
+
if (url)
|
|
105
|
+
return { action: 'comment_list', platform: 'mcmod', type, url, page, size };
|
|
106
|
+
if (query)
|
|
107
|
+
return { action: 'comment_list', platform: 'mcmod', type, query, page, size };
|
|
108
|
+
return { action: 'ignore' };
|
|
109
|
+
}
|
|
79
110
|
if (!query)
|
|
80
111
|
return { action: 'ignore' };
|
|
81
|
-
|
|
82
|
-
const type = normalizeType(raw === null || raw === void 0 ? void 0 : raw.type, platform);
|
|
83
|
-
return { action: 'search', platform, type, query };
|
|
112
|
+
return { action: 'search', platform, type, query, direct: (raw === null || raw === void 0 ? void 0 : raw.direct) !== false };
|
|
84
113
|
}
|
|
85
114
|
async function requestAi(config, text) {
|
|
86
115
|
var _a, _b, _c, _d, _e, _f;
|
|
@@ -97,10 +126,14 @@ async function requestAi(config, text) {
|
|
|
97
126
|
role: 'system',
|
|
98
127
|
content: [
|
|
99
128
|
'你是 Minecraft 模组搜索意图解析器,只返回 JSON,不要解释。',
|
|
100
|
-
'JSON
|
|
129
|
+
'JSON 格式一: {"action":"search|ignore","platform":"mcmod|cf|mr","type":"mod|pack|data|tutorial|author|user|resource|shader|plugin","query":"关键词"}',
|
|
130
|
+
'JSON 格式二: {"action":"comment_list","url":"MCMod页面URL","page":1,"size":5},用于渲染某页面全部主评论列表。',
|
|
131
|
+
'JSON 格式三: {"action":"comment_thread","url":"MCMod页面URL","target":"3楼|floor:3|id:2112330","page":1,"size":5},用于渲染某一楼评论及子评论。',
|
|
132
|
+
'如果用户要求某个模组/作者/教程的评论但没有 URL,返回 {"action":"comment_list","type":"mod|pack|tutorial|author|user","query":"关键词"};如果还指定楼层,再用 comment_thread 并包含 target。',
|
|
101
133
|
'默认 platform 为 mcmod,默认 type 为 mod。',
|
|
102
134
|
'cf/curseforge 表示 CurseForge,mr/modrinth 表示 Modrinth,cnmc/mcmod/MC百科 表示 mcmod.cn。',
|
|
103
|
-
'
|
|
135
|
+
'“查询/搜索/查一下/找一下/模组/评论”等只是意图词,不要放进 query;保留具体模组名、英文名、ID 或关键词。',
|
|
136
|
+
'当用户说“JEI模组”“JEI本体”“查JEI”时,query 应为 "JEI",不要扩展成 JEI 附属或更泛的关键词。',
|
|
104
137
|
'如果不是搜索请求或没有明确关键词,返回 {"action":"ignore"}。',
|
|
105
138
|
].join('\n'),
|
|
106
139
|
},
|
|
@@ -193,7 +226,34 @@ function isExplicitCommand(text, prefixes) {
|
|
|
193
226
|
});
|
|
194
227
|
}
|
|
195
228
|
function buildCommand(decision, prefixes) {
|
|
196
|
-
if (!decision || decision.action
|
|
229
|
+
if (!decision || decision.action === 'ignore')
|
|
230
|
+
return '';
|
|
231
|
+
const cnmcPrefix = (prefixes === null || prefixes === void 0 ? void 0 : prefixes.cnmc) || 'cnmc';
|
|
232
|
+
if (decision.action === 'comment_thread') {
|
|
233
|
+
const page = Math.max(1, Number(decision.page || 1) || 1);
|
|
234
|
+
const size = Number(decision.size || 0) || 0;
|
|
235
|
+
if (decision.url) {
|
|
236
|
+
return `${cnmcPrefix}.comment ${decision.url} ${decision.target} ${page}${size > 0 ? ` -s ${Math.floor(size)}` : ''}`;
|
|
237
|
+
}
|
|
238
|
+
if (decision.query) {
|
|
239
|
+
const type = normalizeType(decision.type, 'mcmod');
|
|
240
|
+
return `${cnmcPrefix}.${type} --direct --comment-target ${decision.target}${size > 0 ? ` -s ${Math.floor(size)}` : ''} ${decision.query}`;
|
|
241
|
+
}
|
|
242
|
+
return '';
|
|
243
|
+
}
|
|
244
|
+
if (decision.action === 'comment_list') {
|
|
245
|
+
const page = Math.max(1, Number(decision.page || 1) || 1);
|
|
246
|
+
const size = Number(decision.size || 0) || 0;
|
|
247
|
+
if (decision.url) {
|
|
248
|
+
return `${cnmcPrefix}.comments ${decision.url} ${page}${size > 0 ? ` -s ${Math.floor(size)}` : ''}`;
|
|
249
|
+
}
|
|
250
|
+
if (decision.query) {
|
|
251
|
+
const type = normalizeType(decision.type, 'mcmod');
|
|
252
|
+
return `${cnmcPrefix}.${type} --direct --comments${size > 0 ? ` -s ${Math.floor(size)}` : ''} ${decision.query}`;
|
|
253
|
+
}
|
|
254
|
+
return '';
|
|
255
|
+
}
|
|
256
|
+
if (decision.action !== 'search')
|
|
197
257
|
return '';
|
|
198
258
|
const platform = normalizePlatform(decision.platform);
|
|
199
259
|
const type = normalizeType(decision.type, platform);
|
|
@@ -205,7 +265,8 @@ function buildCommand(decision, prefixes) {
|
|
|
205
265
|
: platform === 'cf'
|
|
206
266
|
? ((prefixes === null || prefixes === void 0 ? void 0 : prefixes.cf) || 'cf')
|
|
207
267
|
: ((prefixes === null || prefixes === void 0 ? void 0 : prefixes.mr) || 'mr');
|
|
208
|
-
|
|
268
|
+
const direct = decision.direct !== false ? '--direct ' : '';
|
|
269
|
+
return `${prefix}.${type} ${direct}${query}`;
|
|
209
270
|
}
|
|
210
271
|
function apply(ctx, config, shared = {}) {
|
|
211
272
|
if (!(config === null || config === void 0 ? void 0 : config.enabled))
|