koishi-plugin-cfmrmod 1.1.9 → 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.
@@ -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 handleSearch = async (session, platform, type, keyword, useEnglish = false) => {
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
- let detailData;
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
- let detailData;
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
+ }
@@ -9,6 +9,7 @@ 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();
@@ -196,6 +197,57 @@ function apply(ctx, config) {
196
197
  commentListStates.set(session.cid, nextState);
197
198
  return nextState;
198
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
+ }
199
251
  // --- 注册指令 ---
200
252
  const prefix = ((_a = config === null || config === void 0 ? void 0 : config.prefixes) === null || _a === void 0 ? void 0 : _a.cnmc) || 'cnmc';
201
253
  const commandTypes = ['mod', 'data', 'pack', 'tutorial', 'author', 'user'];
@@ -204,6 +256,7 @@ function apply(ctx, config) {
204
256
  `${prefix}.mod/.data/.pack/.tutorial/.author/.user <关键词>`,
205
257
  `${prefix}.comment <url> <楼层|id:评论ID> [page] | 渲染评论与子评论`,
206
258
  `${prefix}.comments <url> [page] | 渲染页面主评论列表`,
259
+ '选项:--direct 精确命中时直接出图,--comments 精确命中时渲染评论列表',
207
260
  '列表交互:输入序号查看,n 下一页,p 上一页,q 退出',
208
261
  ].join('\n'));
209
262
  ctx.command(`${prefix}.comment <url:string> <target:string> [page:number]`, '渲染 MCMod 评论与子评论')
@@ -263,40 +316,35 @@ function apply(ctx, config) {
263
316
  });
264
317
  commandTypes.forEach(type => {
265
318
  ctx.command(`${prefix}.${type} <keyword:text>`)
266
- .action(async ({ session }, keyword) => {
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) => {
267
324
  if (!keyword)
268
325
  return '请输入关键词。';
269
326
  // 将搜索任务加入队列
270
327
  enqueue(session, `search-${type}`, async () => {
271
- var _a;
272
328
  try {
273
329
  if (config.debug)
274
330
  logger.debug(`[${session.userId}] 正在搜索 ${keyword} ...`);
275
- let results = await (0, search_1.fetchSearch)(keyword, type);
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;
276
336
  if (!results.length) {
277
337
  await session.send('未找到相关结果。(备用也没用,我劝你换个关键词试试)');
278
338
  return;
279
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
+ }
280
345
  // 单结果直接处理
281
346
  if (results.length === 1) {
282
- const item = results[0];
283
- await (0, http_1.ensureValidCookie)();
284
- let img;
285
- if (type === 'author')
286
- img = await (0, cards_1.drawAuthorCard)(item.link);
287
- else if (type === 'user') {
288
- const uid = ((_a = item.link.match(/\/(\d+)(?:\.html|\/)?$/)) === null || _a === void 0 ? void 0 : _a[1]) || '0';
289
- img = await (0, cards_1.drawCenterCardImpl)(uid, logger);
290
- }
291
- else if (type === 'mod' || type === 'pack')
292
- img = await (0, cards_1.drawModCard)(item.link);
293
- else if (type === 'tutorial')
294
- img = await (0, cards_1.drawTutorialCard)(item.link);
295
- else
296
- img = await (0, cards_1.createInfoCard)(item.link, type);
297
- await session.send(h.image(await (0, utils_1.toImageSrc)(img)));
298
- if (config.sendLink)
299
- await session.send(`链接: ${item.link}`);
347
+ await renderDirectSearchTarget(session, results[0], type, options);
300
348
  return;
301
349
  }
302
350
  // 多结果:初始化状态(隔离在 session.cid)
@@ -319,25 +367,33 @@ function apply(ctx, config) {
319
367
  });
320
368
  });
321
369
  ctx.command(`${prefix} <keyword:text>`)
322
- .action(async ({ session }, keyword) => {
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) => {
323
375
  if (!keyword)
324
376
  return '请输入关键词。';
325
377
  enqueue(session, 'search-mod', async () => {
326
378
  try {
327
379
  if (config.debug)
328
380
  logger.debug(`[${session.userId}] 正在搜索 ${keyword} ...`);
329
- const results = await (0, search_1.fetchSearch)(keyword, 'mod');
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;
330
386
  if (!results.length) {
331
387
  await session.send('未找到相关结果。(备用也没用,我劝你换个关键词试试)');
332
388
  return;
333
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
+ }
334
395
  if (results.length === 1) {
335
- const item = results[0];
336
- await (0, http_1.ensureValidCookie)();
337
- const img = await (0, cards_1.drawModCard)(item.link);
338
- await session.send(h.image(await (0, utils_1.toImageSrc)(img)));
339
- if (config.sendLink)
340
- await session.send(`链接: ${item.link}`);
396
+ await renderDirectSearchTarget(session, results[0], 'mod', options);
341
397
  return;
342
398
  }
343
399
  clearState(session.cid);
@@ -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)
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 action = String((raw === null || raw === void 0 ? void 0 : raw.action) || '').trim().toLowerCase();
76
- if (action && action !== 'search')
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
- const platform = normalizePlatform(raw === null || raw === void 0 ? void 0 : raw.platform);
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 格式: {"action":"search|ignore","platform":"mcmod|cf|mr","type":"mod|pack|data|tutorial|author|user|resource|shader|plugin","query":"关键词"}',
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
- '“查询/搜索/查一下/找一下/模组”等只是意图词,不要放进 query;保留具体模组名、英文名、ID 或关键词。',
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 !== 'search')
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
- return `${prefix}.${type} ${query}`;
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))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-cfmrmod",
3
- "version": "1.1.9",
3
+ "version": "1.1.10",
4
4
  "description": "Koishi 插件:搜索 CurseForge/Modrinth/MCMod 并渲染图片卡片",
5
5
  "main": "dist/index.js",
6
6
  "files": [