koishi-plugin-cfmrmod 1.1.2 → 1.1.4

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.
@@ -0,0 +1,388 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GLOBAL_FONT_FAMILY = exports.loadImage = exports.createCanvas = void 0;
4
+ exports.configureRenderer = configureRenderer;
5
+ exports.loadImageWithHeaders = loadImageWithHeaders;
6
+ exports.drawTextWithTwemoji = drawTextWithTwemoji;
7
+ exports.roundRect = roundRect;
8
+ exports.wrapText = wrapText;
9
+ exports.measureTableLayout = measureTableLayout;
10
+ exports.drawTable = drawTable;
11
+ const constants_1 = require("./constants");
12
+ const http_1 = require("./http");
13
+ const fs = require('fs');
14
+ const path = require('path');
15
+ let registerFont;
16
+ let RENDER_DEBUG = false;
17
+ let RENDER_TWEMOJI = true;
18
+ let RENDER_TWEMOJI_CDN = 'https://cdn.jsdelivr.net/gh/twitter/twemoji@14.0.2/assets/72x72';
19
+ let RENDER_IMAGE_FETCH_WITH_HEADERS = true;
20
+ exports.GLOBAL_FONT_FAMILY = 'sans-serif';
21
+ const imageBufferCache = new Map();
22
+ const twemojiImageCache = new Map();
23
+ function configureRenderer(canvasService, config, logger) {
24
+ var _a, _b, _c, _d, _e, _f, _g;
25
+ RENDER_DEBUG = !!(config === null || config === void 0 ? void 0 : config.debug);
26
+ RENDER_TWEMOJI = ((_b = (_a = config === null || config === void 0 ? void 0 : config.render) === null || _a === void 0 ? void 0 : _a.emoji) === null || _b === void 0 ? void 0 : _b.twemoji) !== false;
27
+ RENDER_TWEMOJI_CDN = String(((_d = (_c = config === null || config === void 0 ? void 0 : config.render) === null || _c === void 0 ? void 0 : _c.emoji) === null || _d === void 0 ? void 0 : _d.cdn) || RENDER_TWEMOJI_CDN).replace(/\/+$/, '');
28
+ RENDER_IMAGE_FETCH_WITH_HEADERS = ((_f = (_e = config === null || config === void 0 ? void 0 : config.render) === null || _e === void 0 ? void 0 : _e.image) === null || _f === void 0 ? void 0 : _f.fetchWithHeaders) !== false;
29
+ if (!(canvasService === null || canvasService === void 0 ? void 0 : canvasService.createCanvas) || !(canvasService === null || canvasService === void 0 ? void 0 : canvasService.loadImage)) {
30
+ logger.warn('缺少 @napi-rs/canvas,cnmc 指令图片功能已禁用。请在 Koishi 实例目录执行: npm i @napi-rs/canvas');
31
+ return false;
32
+ }
33
+ exports.createCanvas = (w, h) => {
34
+ const width = Math.max(1, Number(w) || 1);
35
+ const height = Math.max(1, Number(h) || 1);
36
+ const canvas = canvasService.createCanvas(width, height);
37
+ if (!canvas || typeof canvas.getContext !== 'function') {
38
+ throw new Error('canvas 服务异常:Canvas 无效');
39
+ }
40
+ return canvas;
41
+ };
42
+ exports.loadImage = canvasService.loadImage;
43
+ registerFont = (fontPath, options) => {
44
+ var _a, _b;
45
+ const family = (options === null || options === void 0 ? void 0 : options.family) || 'MCModFont';
46
+ if (typeof canvasService.registerFont === 'function') {
47
+ return canvasService.registerFont(fontPath, family);
48
+ }
49
+ return (_b = (_a = canvasService.GlobalFonts) === null || _a === void 0 ? void 0 : _a.registerFromPath) === null || _b === void 0 ? void 0 : _b.call(_a, fontPath, family);
50
+ };
51
+ initFont(config === null || config === void 0 ? void 0 : config.fontPath, logger, registerFont);
52
+ try {
53
+ const families = Array.from(((_g = canvasService === null || canvasService === void 0 ? void 0 : canvasService.GlobalFonts) === null || _g === void 0 ? void 0 : _g.families) || []);
54
+ if (families.length) {
55
+ const names = families.slice(0, 10).map((family) => String((family === null || family === void 0 ? void 0 : family.family) || (family === null || family === void 0 ? void 0 : family.name) || family));
56
+ logger.info(`[Font] 当前可用字体: ${names.join(', ')}`);
57
+ }
58
+ }
59
+ catch { }
60
+ return true;
61
+ }
62
+ async function loadImageWithHeaders(url, referer = constants_1.BASE_URL, timeout = 15000) {
63
+ if (!url)
64
+ throw new Error('empty image url');
65
+ if (!RENDER_IMAGE_FETCH_WITH_HEADERS)
66
+ return (0, exports.loadImage)(url);
67
+ const cacheKey = `${url}::${referer}`;
68
+ const cached = imageBufferCache.get(cacheKey);
69
+ if (cached)
70
+ return (0, exports.loadImage)(cached);
71
+ const tried = [];
72
+ const tryUrls = [url];
73
+ const lower = String(url).toLowerCase();
74
+ if (lower.includes('.webp') || lower.includes('format=webp')) {
75
+ tryUrls.push(`https://wsrv.nl/?url=${encodeURIComponent(url)}&output=png`);
76
+ }
77
+ if (!tryUrls.some(candidate => candidate.includes('wsrv.nl'))) {
78
+ tryUrls.push(`https://wsrv.nl/?url=${encodeURIComponent(url)}`);
79
+ }
80
+ let lastErr = null;
81
+ for (const attemptUrl of tryUrls) {
82
+ if (tried.includes(attemptUrl))
83
+ continue;
84
+ tried.push(attemptUrl);
85
+ for (let i = 0; i < 2; i++) {
86
+ const fetchModes = [
87
+ { name: 'direct', opts: { agent: false } },
88
+ { name: 'default', opts: {} },
89
+ ];
90
+ for (const mode of fetchModes) {
91
+ try {
92
+ const res = await (0, http_1.fetchWithTimeout)(attemptUrl, { headers: (0, http_1.getImageHeaders)(attemptUrl, referer), ...mode.opts }, timeout);
93
+ if (!res.ok)
94
+ throw new Error(`HTTP ${res.status}`);
95
+ const buf = await res.buffer();
96
+ const img = await (0, exports.loadImage)(buf);
97
+ imageBufferCache.set(cacheKey, buf);
98
+ return img;
99
+ }
100
+ catch (e) {
101
+ lastErr = e;
102
+ if (RENDER_DEBUG)
103
+ console.warn(`[mcmod] image fail (${i + 1}/2, ${mode.name}): ${attemptUrl} -> ${e.message}`);
104
+ }
105
+ }
106
+ }
107
+ }
108
+ throw lastErr || new Error('loadImageWithHeaders failed');
109
+ }
110
+ function emojiToTwemojiUrl(emoji) {
111
+ const codepoints = [];
112
+ for (const ch of Array.from(String(emoji || ''))) {
113
+ const cp = ch.codePointAt(0);
114
+ if (!cp)
115
+ continue;
116
+ if (cp === 0xfe0f)
117
+ continue;
118
+ codepoints.push(cp.toString(16));
119
+ }
120
+ if (!codepoints.length)
121
+ return null;
122
+ return `${RENDER_TWEMOJI_CDN}/${codepoints.join('-')}.png`;
123
+ }
124
+ async function loadTwemojiImage(emoji) {
125
+ if (!RENDER_TWEMOJI)
126
+ return null;
127
+ const key = String(emoji || '');
128
+ if (!key)
129
+ return null;
130
+ if (twemojiImageCache.has(key))
131
+ return twemojiImageCache.get(key);
132
+ const promise = (async () => {
133
+ const url = emojiToTwemojiUrl(key);
134
+ if (!url)
135
+ return null;
136
+ try {
137
+ return await loadImageWithHeaders(url, RENDER_TWEMOJI_CDN, 12000);
138
+ }
139
+ catch {
140
+ return null;
141
+ }
142
+ })();
143
+ twemojiImageCache.set(key, promise);
144
+ return promise;
145
+ }
146
+ function splitTextUnits(text) {
147
+ const emojiRegex = /\p{Extended_Pictographic}/u;
148
+ const IntlAny = globalThis.Intl;
149
+ const seg = (IntlAny === null || IntlAny === void 0 ? void 0 : IntlAny.Segmenter) ? new IntlAny.Segmenter('zh', { granularity: 'grapheme' }) : null;
150
+ const graphemes = seg ? Array.from(seg.segment(String(text || '')), (s) => s.segment) : Array.from(String(text || ''));
151
+ return graphemes.map((grapheme) => ({ type: emojiRegex.test(grapheme) ? 'emoji' : 'text', val: grapheme }));
152
+ }
153
+ async function drawTextWithTwemoji(ctx, text, x, y, maxWidth, lineHeight, maxLines = 1000, draw = true) {
154
+ if (!text)
155
+ return y;
156
+ const paragraphs = String(text).replace(/\r/g, '').split('\n');
157
+ const emojiSize = Math.max(14, Math.floor(lineHeight * 0.9));
158
+ let currentY = y;
159
+ let lines = 0;
160
+ const drawLine = async (units) => {
161
+ let cx = x;
162
+ for (const unit of units) {
163
+ if (unit.type === 'emoji') {
164
+ if (draw) {
165
+ const img = await loadTwemojiImage(unit.val);
166
+ if (img)
167
+ ctx.drawImage(img, cx, currentY + Math.max(0, Math.floor((lineHeight - emojiSize) / 2)), emojiSize, emojiSize);
168
+ else
169
+ ctx.fillText(unit.val, cx, currentY);
170
+ }
171
+ cx += emojiSize;
172
+ }
173
+ else {
174
+ if (draw)
175
+ ctx.fillText(unit.val, cx, currentY);
176
+ cx += ctx.measureText(unit.val).width;
177
+ }
178
+ }
179
+ currentY += lineHeight;
180
+ lines++;
181
+ };
182
+ for (const paragraph of paragraphs) {
183
+ const units = splitTextUnits(paragraph);
184
+ let line = [];
185
+ let lineW = 0;
186
+ for (const unit of units) {
187
+ const w = unit.type === 'emoji' ? emojiSize : ctx.measureText(unit.val).width;
188
+ if (lineW + w > maxWidth && line.length) {
189
+ await drawLine(line);
190
+ if (lines >= maxLines)
191
+ return currentY;
192
+ line = [];
193
+ lineW = 0;
194
+ }
195
+ line.push(unit);
196
+ lineW += w;
197
+ }
198
+ if (line.length) {
199
+ await drawLine(line);
200
+ if (lines >= maxLines)
201
+ return currentY;
202
+ }
203
+ else {
204
+ currentY += lineHeight;
205
+ lines++;
206
+ }
207
+ }
208
+ return currentY;
209
+ }
210
+ function roundRect(ctx, x, y, w, h, r) {
211
+ if (w < 2 * r)
212
+ r = w / 2;
213
+ if (h < 2 * r)
214
+ r = h / 2;
215
+ ctx.beginPath();
216
+ ctx.moveTo(x + r, y);
217
+ ctx.arcTo(x + w, y, x + w, y + h, r);
218
+ ctx.arcTo(x + w, y + h, x, y + h, r);
219
+ ctx.arcTo(x, y + h, x, y, r);
220
+ ctx.arcTo(x, y, x + w, y, r);
221
+ ctx.closePath();
222
+ }
223
+ function wrapText(ctx, text, x, y, maxWidth, lineHeight, maxLines = 1000, draw = true) {
224
+ if (!text)
225
+ return y;
226
+ const IntlAny = globalThis.Intl;
227
+ const seg = (IntlAny === null || IntlAny === void 0 ? void 0 : IntlAny.Segmenter) ? new IntlAny.Segmenter('zh', { granularity: 'grapheme' }) : null;
228
+ const splitGraphemes = (value) => {
229
+ if (!value)
230
+ return [];
231
+ if (seg)
232
+ return Array.from(seg.segment(value), (item) => item.segment);
233
+ return Array.from(value);
234
+ };
235
+ const paragraphs = String(text).replace(/\r/g, '').split('\n');
236
+ let linesCount = 0;
237
+ let currentY = y;
238
+ const flush = (line) => {
239
+ if (draw && line)
240
+ ctx.fillText(line, x, currentY);
241
+ currentY += lineHeight;
242
+ linesCount++;
243
+ };
244
+ for (const paragraph of paragraphs) {
245
+ const tokens = paragraph.match(/https?:\/\/\S+|\s+|[^\s]/gu) || [];
246
+ let line = '';
247
+ for (const token of tokens) {
248
+ const next = line + token;
249
+ if (ctx.measureText(next).width <= maxWidth || !line) {
250
+ line = next;
251
+ continue;
252
+ }
253
+ flush(line.trimEnd());
254
+ if (linesCount >= maxLines)
255
+ return currentY;
256
+ line = token.trimStart();
257
+ while (line && ctx.measureText(line).width > maxWidth) {
258
+ const glyphs = splitGraphemes(line);
259
+ const head = glyphs.shift();
260
+ let chunk = head || '';
261
+ while (glyphs.length && ctx.measureText(chunk + glyphs[0]).width <= maxWidth)
262
+ chunk += glyphs.shift();
263
+ flush(chunk);
264
+ if (linesCount >= maxLines)
265
+ return currentY;
266
+ line = glyphs.join('');
267
+ }
268
+ }
269
+ if (line) {
270
+ flush(line.trimEnd());
271
+ if (linesCount >= maxLines)
272
+ return currentY;
273
+ }
274
+ else {
275
+ currentY += lineHeight;
276
+ }
277
+ }
278
+ return currentY;
279
+ }
280
+ function measureTableLayout(ctx, table, maxWidth, lineHeight, font, headerFont) {
281
+ const rows = Array.isArray(table === null || table === void 0 ? void 0 : table.rows) ? table.rows : [];
282
+ if (!rows.length)
283
+ return null;
284
+ const colCount = Math.max(...rows.map(row => row.length), 1);
285
+ const padX = 10;
286
+ const padY = 8;
287
+ const minCol = 80;
288
+ const maxCol = 320;
289
+ const colWidths = Array(colCount).fill(minCol);
290
+ for (let col = 0; col < colCount; col++) {
291
+ let maxW = minCol;
292
+ rows.forEach((row, rowIndex) => {
293
+ var _a;
294
+ const text = String((_a = row[col]) !== null && _a !== void 0 ? _a : '');
295
+ ctx.font = rowIndex === 0 ? headerFont : font;
296
+ maxW = Math.max(maxW, Math.min(maxCol, ctx.measureText(text).width + padX * 2));
297
+ });
298
+ colWidths[col] = maxW;
299
+ }
300
+ const rawW = colWidths.reduce((sum, width) => sum + width, 0);
301
+ if (rawW > maxWidth) {
302
+ const scale = maxWidth / rawW;
303
+ for (let i = 0; i < colWidths.length; i++)
304
+ colWidths[i] = Math.max(60, Math.floor(colWidths[i] * scale));
305
+ }
306
+ const rowHeights = rows.map((row, rowIndex) => {
307
+ var _a;
308
+ let rowH = lineHeight + padY * 2;
309
+ for (let col = 0; col < colCount; col++) {
310
+ const text = String((_a = row[col]) !== null && _a !== void 0 ? _a : '');
311
+ const colWidth = Math.max(20, colWidths[col] - padX * 2);
312
+ ctx.font = rowIndex === 0 ? headerFont : font;
313
+ const height = wrapText(ctx, text, 0, 0, colWidth, lineHeight, 1000, false);
314
+ rowH = Math.max(rowH, height + padY * 2);
315
+ }
316
+ return rowH;
317
+ });
318
+ return {
319
+ colWidths,
320
+ rowHeights,
321
+ totalW: colWidths.reduce((sum, width) => sum + width, 0),
322
+ totalH: rowHeights.reduce((sum, height) => sum + height, 0),
323
+ padX,
324
+ padY,
325
+ };
326
+ }
327
+ function drawTable(ctx, table, x, y, maxWidth, lineHeight, font, headerFont, colors) {
328
+ var _a;
329
+ const layout = measureTableLayout(ctx, table, maxWidth, lineHeight, font, headerFont);
330
+ if (!layout)
331
+ return 0;
332
+ const { colWidths, rowHeights, padX, padY } = layout;
333
+ const rows = table.rows;
334
+ let cy = y;
335
+ for (let row = 0; row < rows.length; row++) {
336
+ let cx = x;
337
+ const rowHeight = rowHeights[row];
338
+ for (let col = 0; col < colWidths.length; col++) {
339
+ const colWidth = colWidths[col];
340
+ ctx.fillStyle = row === 0 ? colors.headerBg : colors.cellBg;
341
+ ctx.fillRect(cx, cy, colWidth, rowHeight);
342
+ ctx.strokeStyle = colors.border;
343
+ ctx.lineWidth = 1;
344
+ ctx.strokeRect(cx, cy, colWidth, rowHeight);
345
+ ctx.fillStyle = colors.text;
346
+ ctx.font = row === 0 ? headerFont : font;
347
+ wrapText(ctx, String((_a = rows[row][col]) !== null && _a !== void 0 ? _a : ''), cx + padX, cy + padY, colWidth - padX * 2, lineHeight, 1000, true);
348
+ cx += colWidth;
349
+ }
350
+ cy += rowHeight;
351
+ }
352
+ return layout.totalH;
353
+ }
354
+ function initFont(preferredPath, logger, registerFontFn) {
355
+ const fontName = 'MCModFont';
356
+ const tryRegister = (filePath, source) => {
357
+ if (!fs.existsSync(filePath))
358
+ return false;
359
+ try {
360
+ if (registerFontFn) {
361
+ registerFontFn(filePath, { family: fontName });
362
+ exports.GLOBAL_FONT_FAMILY = fontName;
363
+ logger.info(`[Font] 成功加载${source}: ${filePath}`);
364
+ return true;
365
+ }
366
+ }
367
+ catch (e) { }
368
+ return false;
369
+ };
370
+ if (preferredPath) {
371
+ const abs = path.isAbsolute(preferredPath) ? preferredPath : path.resolve(process.cwd(), preferredPath);
372
+ if (tryRegister(abs, '配置字体'))
373
+ return true;
374
+ }
375
+ const candidates = [
376
+ 'C:\\Windows\\Fonts\\msyh.ttc', 'C:\\Windows\\Fonts\\msyh.ttf', 'C:\\Windows\\Fonts\\simhei.ttf',
377
+ 'C:\\Windows\\Fonts\\seguiemj.ttf',
378
+ '/usr/share/fonts/truetype/noto/NotoSansSC-Regular.otf', '/usr/share/fonts/noto/NotoSansSC-Regular.otf',
379
+ '/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf', '/usr/share/fonts/noto/NotoColorEmoji.ttf',
380
+ '/usr/share/fonts/wqy-zenhei/wqy-zenhei.ttc', '/System/Library/Fonts/PingFang.ttc',
381
+ '/System/Library/Fonts/Apple Color Emoji.ttc',
382
+ ];
383
+ for (const candidate of candidates) {
384
+ if (tryRegister(candidate, '系统字体'))
385
+ return true;
386
+ }
387
+ return false;
388
+ }
@@ -0,0 +1,122 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.fetchSearch = fetchSearch;
4
+ exports.fetchSearchFallback = fetchSearchFallback;
5
+ exports.formatListPage = formatListPage;
6
+ const constants_1 = require("./constants");
7
+ const http_1 = require("./http");
8
+ const utils_1 = require("./utils");
9
+ const cheerio = require('cheerio');
10
+ async function fetchSearch(query, typeKey) {
11
+ const filterMap = { mod: 1, pack: 2, data: 3, tutorial: 4, author: 5, user: 6 };
12
+ const filter = filterMap[typeKey] || 1;
13
+ const searchUrl = `https://search.mcmod.cn/s?key=${encodeURIComponent(query)}&filter=${filter}&mold=0`;
14
+ let results = [];
15
+ try {
16
+ const html = await (0, http_1.fetchMcmodText)(searchUrl, { headers: (0, http_1.getHeaders)('https://search.mcmod.cn/') });
17
+ const $ = cheerio.load(html);
18
+ $('.result-item, .media, .search-list .item, .user-list .row, .list .row').each((i, el) => {
19
+ const $el = $(el);
20
+ let titleEl = $el.find('.head > a').first();
21
+ if (!titleEl.length)
22
+ titleEl = $el.find('.media-heading a').first();
23
+ if (!titleEl.length) {
24
+ $el.find('a').each((j, a) => {
25
+ if ($(a).text().trim().length > 0 && !titleEl.length)
26
+ titleEl = $(a);
27
+ });
28
+ }
29
+ const title = (0, utils_1.cleanText)(titleEl.text());
30
+ let link = titleEl.attr('href');
31
+ const modName = (0, utils_1.cleanText)($el.find('.meta span, .source').first().text()) || (0, utils_1.cleanText)($el.find('.media-body .text-muted').first().text());
32
+ if (title && link) {
33
+ link = (0, utils_1.fixUrl)(link);
34
+ if (link && !link.includes('target=') && !/^\d+$/.test(title)) {
35
+ let summary = (0, utils_1.cleanText)($el.find('.body, .media-body').text());
36
+ summary = summary.replace(title, '').replace(modName, '').trim();
37
+ results.push({ title, link, modName: modName || '', summary });
38
+ }
39
+ }
40
+ });
41
+ }
42
+ catch (e) {
43
+ // 主站搜索失败忽略,继续走备用
44
+ }
45
+ if (results.length === 0) {
46
+ try {
47
+ const fallbackResults = await fetchSearchFallback(query, typeKey);
48
+ if (fallbackResults && fallbackResults.length > 0) {
49
+ return fallbackResults;
50
+ }
51
+ }
52
+ catch (e) {
53
+ // 备用接口失败则彻底无结果
54
+ }
55
+ }
56
+ return results;
57
+ }
58
+ async function fetchSearchFallback(query, typeKey) {
59
+ const apiType = constants_1.FALLBACK_TYPE_MAP[typeKey];
60
+ if (!apiType)
61
+ return [];
62
+ try {
63
+ const requestData = { key: query, type: apiType };
64
+ const params = new URLSearchParams();
65
+ params.append('data', JSON.stringify(requestData));
66
+ const headers = {
67
+ ...(0, http_1.getHeaders)('https://www.mcmod.cn'),
68
+ 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8',
69
+ };
70
+ const json = await (0, http_1.fetchMcmodJson)(constants_1.COMMON_SELECT_URL, {
71
+ method: 'POST',
72
+ headers,
73
+ body: params,
74
+ });
75
+ if (json.state === 0 && json.html) {
76
+ const $ = cheerio.load(json.html);
77
+ const results = [];
78
+ $('tr[data-id]').each((i, el) => {
79
+ const $el = $(el);
80
+ const id = $el.attr('data-id');
81
+ if (!id)
82
+ return;
83
+ let title = '';
84
+ let summary = '(来自快速索引)';
85
+ let link = '';
86
+ if (typeKey === 'author') {
87
+ title = (0, utils_1.cleanText)($el.find('b').text()) || (0, utils_1.cleanText)($el.text());
88
+ summary = (0, utils_1.cleanText)($el.find('i').text());
89
+ link = `https://www.mcmod.cn/author/${id}.html`;
90
+ }
91
+ else {
92
+ const rawText = (0, utils_1.cleanText)($el.text());
93
+ title = rawText.replace(/^ID:\d+\s*/, '');
94
+ link = `https://www.mcmod.cn/class/${id}.html`;
95
+ summary = `ID: ${id}`;
96
+ }
97
+ if (title && link) {
98
+ results.push({
99
+ title,
100
+ link,
101
+ modName: typeKey === 'pack' ? '整合包' : '',
102
+ summary,
103
+ });
104
+ }
105
+ });
106
+ return results;
107
+ }
108
+ }
109
+ catch (e) {
110
+ // console.error('备用接口解析失败:', e);
111
+ }
112
+ return [];
113
+ }
114
+ function formatListPage(items, pageIndex, type) {
115
+ const total = Math.max(1, Math.ceil(items.length / constants_1.PAGE_SIZE));
116
+ const page = items.slice(pageIndex * constants_1.PAGE_SIZE, (pageIndex + 1) * constants_1.PAGE_SIZE);
117
+ const typeName = { mod: '模组', pack: '整合包', data: '资料', tutorial: '教程', author: '作者', user: '用户' }[type] || '结果';
118
+ let text = `[mcmod] 搜索到的${typeName} (第 ${pageIndex + 1}/${total} 页):\n`;
119
+ page.forEach((it, idx) => text += `${(pageIndex * constants_1.PAGE_SIZE) + idx + 1}. ${it.title}${it.modName ? ` 《${it.modName.replace(/[《》]/g, '')}》` : ''}\n`);
120
+ text += '\n发送序号选择,p/n 翻页,q 退出。';
121
+ return text;
122
+ }
@@ -0,0 +1,97 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.toImageSrc = toImageSrc;
4
+ exports.cleanText = cleanText;
5
+ exports.fixUrl = fixUrl;
6
+ exports.compactUrlText = compactUrlText;
7
+ exports.extractImageUrl = extractImageUrl;
8
+ exports.parseGalleryFromTable = parseGalleryFromTable;
9
+ const constants_1 = require("./constants");
10
+ async function toImageSrc(input) {
11
+ const value = (input && typeof input.then === 'function') ? await input : input;
12
+ if (!value)
13
+ return '';
14
+ if (typeof value === 'string')
15
+ return value;
16
+ const buf = Buffer.isBuffer(value) ? value : (value instanceof Uint8Array ? Buffer.from(value) : null);
17
+ if (buf)
18
+ return `data:image/png;base64,${buf.toString('base64')}`;
19
+ return String(value);
20
+ }
21
+ function cleanText(text) {
22
+ if (!text)
23
+ return '';
24
+ return text.replace(/[\r\n\t]+/g, '').trim();
25
+ }
26
+ function fixUrl(url) {
27
+ if (!url)
28
+ return null;
29
+ if (url.startsWith('//'))
30
+ return 'https:' + url;
31
+ if (url.startsWith('/'))
32
+ return constants_1.BASE_URL + url;
33
+ if (!url.startsWith('http'))
34
+ return constants_1.BASE_URL + '/' + url;
35
+ return url;
36
+ }
37
+ function compactUrlText(url) {
38
+ if (!url)
39
+ return '';
40
+ const limit = 60;
41
+ let text = String(url).trim();
42
+ try {
43
+ const parsed = new URL(text.startsWith('//') ? `https:${text}` : text);
44
+ const host = parsed.hostname.replace(/^www\./, '');
45
+ const path = parsed.pathname === '/' ? '' : parsed.pathname.replace(/\/$/, '');
46
+ text = `${host}${path}`;
47
+ }
48
+ catch {
49
+ text = text.replace(/^https?:\/\//, '').replace(/^www\./, '').replace(/\/$/, '');
50
+ }
51
+ return text.length > limit ? `${text.slice(0, limit - 3)}...` : text;
52
+ }
53
+ function extractImageUrl(node) {
54
+ var _a;
55
+ if (!node || !node.attribs)
56
+ return null;
57
+ const attrs = node.attribs;
58
+ const candidates = [
59
+ attrs['data-original'],
60
+ attrs['data-lazy-src'],
61
+ attrs['data-src'],
62
+ attrs['src'],
63
+ ].filter(Boolean);
64
+ if (attrs['srcset']) {
65
+ const first = (_a = String(attrs['srcset']).split(',')[0]) === null || _a === void 0 ? void 0 : _a.trim().split(' ')[0];
66
+ if (first)
67
+ candidates.push(first);
68
+ }
69
+ if (attrs['style']) {
70
+ const match = String(attrs['style']).match(/background-image:\s*url\((['"]?)([^'")]+)\1\)/i);
71
+ if (match === null || match === void 0 ? void 0 : match[2])
72
+ candidates.push(match[2]);
73
+ }
74
+ for (const candidate of candidates) {
75
+ const url = fixUrl(String(candidate).trim());
76
+ if (url)
77
+ return url;
78
+ }
79
+ return null;
80
+ }
81
+ function parseGalleryFromTable($, tableNode) {
82
+ const items = [];
83
+ $(tableNode).find('td').each((_, td) => {
84
+ const imgNode = $(td).find('img').first()[0];
85
+ if (!imgNode)
86
+ return;
87
+ const src = extractImageUrl(imgNode);
88
+ if (!src)
89
+ return;
90
+ const caption = cleanText($(td).find('.figcaption, figcaption').first().text()) ||
91
+ cleanText($(td).find('[class*="caption"]').first().text()) ||
92
+ cleanText($(imgNode).attr('alt')) ||
93
+ '';
94
+ items.push({ src: fixUrl(src), caption });
95
+ });
96
+ return items;
97
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-cfmrmod",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "Koishi 插件:搜索 CurseForge/Modrinth/MCMod 并渲染图片卡片",
5
5
  "main": "dist/index.js",
6
6
  "files": [