koishi-plugin-fimtale-api 0.0.95 → 0.0.99
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/index.js +98 -92
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -47,7 +47,7 @@ var Config = import_koishi.Schema.object({
|
|
|
47
47
|
cookies: import_koishi.Schema.string().role("secret").description("浏览器 Cookie (用于解除安全模式,必填)"),
|
|
48
48
|
pollInterval: import_koishi.Schema.number().default(60 * 1e3).description("追更轮询间隔(ms)"),
|
|
49
49
|
autoParseLink: import_koishi.Schema.boolean().default(true).description("自动解析链接为预览卡片"),
|
|
50
|
-
// 渲染配置 (
|
|
50
|
+
// 渲染配置 (标准手机比例)
|
|
51
51
|
deviceWidth: import_koishi.Schema.number().default(390).description("阅读器渲染宽度(px)"),
|
|
52
52
|
deviceHeight: import_koishi.Schema.number().default(844).description("阅读器渲染高度(px)"),
|
|
53
53
|
fontSize: import_koishi.Schema.number().default(20).description("正文字号(px)")
|
|
@@ -61,11 +61,6 @@ function apply(ctx, config) {
|
|
|
61
61
|
lastCheck: "integer"
|
|
62
62
|
}, { primary: "id", autoInc: true });
|
|
63
63
|
const sleep = /* @__PURE__ */ __name((ms) => new Promise((resolve) => setTimeout(resolve, ms)), "sleep");
|
|
64
|
-
const formatDate = /* @__PURE__ */ __name((timestamp) => {
|
|
65
|
-
if (!timestamp) return "Unknown";
|
|
66
|
-
const date = new Date(timestamp * 1e3);
|
|
67
|
-
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
|
68
|
-
}, "formatDate");
|
|
69
64
|
const stripHtml = /* @__PURE__ */ __name((html) => {
|
|
70
65
|
if (!html) return "";
|
|
71
66
|
return html.replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
@@ -83,9 +78,9 @@ function apply(ctx, config) {
|
|
|
83
78
|
}, "generateGradient");
|
|
84
79
|
const cleanContent = /* @__PURE__ */ __name((html) => {
|
|
85
80
|
if (!html) return "";
|
|
86
|
-
return html.replace(/style
|
|
81
|
+
return html.replace(/style\s*=\s*['"][^'"]*['"]/gi, "").replace(/<p[^>]*>\s*(?:<br\s*\/?>| | |\s| )*\s*<\/p>/gi, "").replace(/<br\s*\/?>\s*<\/p>/gi, "</p>").replace(/<\/p>\s*(?:<br\s*\/?>\s*)+\s*<p/gi, "</p><p>").replace(/(?:<br\s*\/?>\s*){2,}/gi, "<br>").replace(/(?:<br\s*\/?>| |\s)+$/gi, "").trim();
|
|
87
82
|
}, "cleanContent");
|
|
88
|
-
const fontStack = '"
|
|
83
|
+
const fontStack = '"Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif';
|
|
89
84
|
const fontSerif = '"Noto Serif SC", "Source Han Serif SC", "SimSun", serif';
|
|
90
85
|
const injectCookies = /* @__PURE__ */ __name(async (page) => {
|
|
91
86
|
if (!config.cookies) return;
|
|
@@ -101,7 +96,7 @@ function apply(ctx, config) {
|
|
|
101
96
|
const url = `${config.apiUrl}/t/${threadId}`;
|
|
102
97
|
const params = { APIKey: config.apiKey, APIPass: config.apiPass };
|
|
103
98
|
const res = await ctx.http.get(url, { params });
|
|
104
|
-
if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API
|
|
99
|
+
if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API Error" };
|
|
105
100
|
return {
|
|
106
101
|
valid: true,
|
|
107
102
|
data: res.TopicInfo,
|
|
@@ -109,7 +104,7 @@ function apply(ctx, config) {
|
|
|
109
104
|
menu: res.Menu || []
|
|
110
105
|
};
|
|
111
106
|
} catch (e) {
|
|
112
|
-
return { valid: false, msg: "
|
|
107
|
+
return { valid: false, msg: "Request Failed" };
|
|
113
108
|
}
|
|
114
109
|
}, "fetchThread");
|
|
115
110
|
const fetchRandomId = /* @__PURE__ */ __name(async () => {
|
|
@@ -149,7 +144,7 @@ function apply(ctx, config) {
|
|
|
149
144
|
const titleEl = card.querySelector(".card-title");
|
|
150
145
|
if (titleEl) title = titleEl.textContent?.trim() || "";
|
|
151
146
|
else title = link.textContent?.trim() || "";
|
|
152
|
-
let author = "
|
|
147
|
+
let author = "Unknown";
|
|
153
148
|
const authorEl = card.querySelector('a[href^="/u/"] span.grey-text');
|
|
154
149
|
if (authorEl) author = authorEl.textContent?.trim() || "";
|
|
155
150
|
let cover = void 0;
|
|
@@ -220,7 +215,7 @@ function apply(ctx, config) {
|
|
|
220
215
|
const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
|
|
221
216
|
let summary = stripHtml(info.Content);
|
|
222
217
|
if (summary.length < 10 && parent && isChapter) summary = stripHtml(parent.Content);
|
|
223
|
-
if (summary.length >
|
|
218
|
+
if (summary.length > 150) summary = summary.substring(0, 150) + "...";
|
|
224
219
|
if (!summary) summary = "暂无简介";
|
|
225
220
|
const tagsArr = [];
|
|
226
221
|
if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
|
|
@@ -238,37 +233,38 @@ function apply(ctx, config) {
|
|
|
238
233
|
.card { width: 620px; height: 360px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
|
|
239
234
|
.cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
|
|
240
235
|
.id-tag { position: absolute; top: 12px; left: 12px; background: rgba(0,0,0,0.6); color: #fff; padding: 4px 10px; border-radius: 6px; font-size: 13px; font-weight: bold; backdrop-filter: blur(4px); font-family: monospace; }
|
|
241
|
-
.info { flex: 1; padding: 24px; display: flex; flex-direction: column;
|
|
236
|
+
.info { flex: 1; padding: 24px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
|
|
242
237
|
|
|
243
|
-
.header-group {
|
|
238
|
+
.header-group { flex-shrink: 0; margin-bottom: 10px; }
|
|
244
239
|
.title {
|
|
245
|
-
font-size: 22px; font-weight: 700; color: #333; line-height: 1.
|
|
240
|
+
font-size: 22px; font-weight: 700; color: #333; line-height: 1.4;
|
|
246
241
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
242
|
+
margin-bottom: 4px;
|
|
247
243
|
}
|
|
248
244
|
.subtitle {
|
|
249
|
-
font-size: 15px; color: #555;
|
|
245
|
+
font-size: 15px; color: #555; font-weight: 500;
|
|
250
246
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
251
247
|
padding-left: 10px; border-left: 3px solid #e91e63;
|
|
248
|
+
margin-top: 4px;
|
|
252
249
|
}
|
|
253
|
-
.author { font-size: 13px; color: #888; margin-top:
|
|
250
|
+
.author { font-size: 13px; color: #888; margin-top: 6px; font-weight: 400; }
|
|
254
251
|
|
|
255
|
-
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px
|
|
252
|
+
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; flex-shrink: 0; }
|
|
256
253
|
.tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
|
|
257
254
|
.tag-imp { background: #e3f2fd; color: #1565c0; }
|
|
258
255
|
|
|
259
|
-
|
|
260
|
-
.summary-box {
|
|
261
|
-
flex: 1; position: relative; margin-top: 4px; overflow: hidden;
|
|
262
|
-
}
|
|
256
|
+
.summary-box { flex: 1; position: relative; overflow: hidden; min-height: 0; }
|
|
263
257
|
.summary {
|
|
264
|
-
font-size: 13px; color: #666;
|
|
265
|
-
line-height: 1.5; /* 设定标准行高 */
|
|
266
|
-
max-height: 6em; /* 4行 = 1.5 * 4 */
|
|
258
|
+
font-size: 13px; color: #666; line-height: 1.6;
|
|
267
259
|
display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
|
|
268
|
-
padding-bottom:
|
|
260
|
+
padding-bottom: 3px;
|
|
269
261
|
}
|
|
270
262
|
|
|
271
|
-
.footer {
|
|
263
|
+
.footer {
|
|
264
|
+
border-top: 1px solid #eee; padding-top: 14px;
|
|
265
|
+
display: flex; justify-content: space-between;
|
|
266
|
+
font-size: 12px; color: #888; margin-top: auto; flex-shrink: 0;
|
|
267
|
+
}
|
|
272
268
|
.stat b { color: #555; font-weight: bold; margin-right: 2px;}
|
|
273
269
|
</style>
|
|
274
270
|
</head>
|
|
@@ -281,13 +277,8 @@ function apply(ctx, config) {
|
|
|
281
277
|
${subTitle ? `<div class="subtitle">${subTitle}</div>` : ""}
|
|
282
278
|
<div class="author">@${info.UserName}</div>
|
|
283
279
|
</div>
|
|
284
|
-
|
|
285
280
|
<div class="tags">${displayTags.map((t) => `<span class="tag ${["文", "译", "R"].includes(t) ? "tag-imp" : ""}">${t}</span>`).join("")}</div>
|
|
286
|
-
|
|
287
|
-
<div class="summary-box">
|
|
288
|
-
<div class="summary">${summary}</div>
|
|
289
|
-
</div>
|
|
290
|
-
|
|
281
|
+
<div class="summary-box"><div class="summary">${summary}</div></div>
|
|
291
282
|
<div class="footer">
|
|
292
283
|
<span class="stat"><b style="color:#009688">热度</b>${views}</span>
|
|
293
284
|
<span class="stat"><b style="color:#673ab7">评论</b>${comments}</span>
|
|
@@ -364,76 +355,90 @@ function apply(ctx, config) {
|
|
|
364
355
|
}, "renderSearchResults");
|
|
365
356
|
const renderReadPages = /* @__PURE__ */ __name(async (info) => {
|
|
366
357
|
const content = cleanContent(info.Content);
|
|
358
|
+
const headerHeight = 40;
|
|
359
|
+
const footerHeight = 30;
|
|
360
|
+
const paddingX = 25;
|
|
361
|
+
const paddingY = 20;
|
|
362
|
+
const lineHeightRatio = 1.8;
|
|
363
|
+
const contentWidth = config.deviceWidth - paddingX * 2;
|
|
364
|
+
const columnGap = 40;
|
|
365
|
+
const maxContentHeight = config.deviceHeight - headerHeight - footerHeight;
|
|
366
|
+
const lineHeightPx = config.fontSize * lineHeightRatio;
|
|
367
|
+
const linesPerPage = Math.floor((maxContentHeight - paddingY * 2) / lineHeightPx);
|
|
368
|
+
const optimalContentHeight = linesPerPage * lineHeightPx + paddingY * 2;
|
|
369
|
+
const marginTop = Math.floor((maxContentHeight - optimalContentHeight) / 2) + headerHeight;
|
|
367
370
|
const html = `
|
|
368
371
|
<!DOCTYPE html>
|
|
369
372
|
<html>
|
|
370
373
|
<head>
|
|
371
374
|
<style>
|
|
372
|
-
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontSerif}; }
|
|
373
|
-
|
|
374
|
-
.
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
/* 关键:CSS多列布局,无需手动分页 */
|
|
380
|
-
display: flex; flex-direction: column;
|
|
375
|
+
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; height: ${config.deviceHeight}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontSerif}; overflow: hidden; position: relative;}
|
|
376
|
+
|
|
377
|
+
.fixed-header {
|
|
378
|
+
position: absolute; top: 0; left: 0; width: 100%; height: ${headerHeight}px;
|
|
379
|
+
border-bottom: 1px solid #d7ccc8; box-sizing: border-box;
|
|
380
|
+
padding: 0 20px; display: flex; align-items: center; justify-content: space-between;
|
|
381
|
+
font-size: 12px; color: #8d6e63; background: #f6f4ec; z-index: 5; font-weight: bold;
|
|
381
382
|
}
|
|
382
383
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
overflow-x: hidden; overflow-y: hidden; /* 隐藏滚动条 */
|
|
384
|
+
.fixed-footer {
|
|
385
|
+
position: absolute; bottom: 0; left: 0; width: 100%; height: ${footerHeight}px;
|
|
386
|
+
display: flex; align-items: center; justify-content: center;
|
|
387
|
+
font-size: 12px; color: #aaa; background: #f6f4ec; z-index: 5;
|
|
388
388
|
}
|
|
389
389
|
|
|
390
|
-
/*
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
390
|
+
/* 视口容器:限定只显示一页内容 */
|
|
391
|
+
#viewport {
|
|
392
|
+
position: absolute;
|
|
393
|
+
top: ${marginTop}px;
|
|
394
|
+
left: ${paddingX}px;
|
|
395
|
+
width: ${contentWidth}px;
|
|
396
|
+
height: ${optimalContentHeight}px;
|
|
397
|
+
overflow: hidden;
|
|
398
398
|
}
|
|
399
399
|
|
|
400
|
-
/*
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
400
|
+
/* 长条容器:包含所有列 */
|
|
401
|
+
#content-scroller {
|
|
402
|
+
height: 100%;
|
|
403
|
+
width: 100%;
|
|
404
|
+
|
|
405
|
+
/* CSS Columns 布局 */
|
|
406
|
+
column-width: ${contentWidth}px;
|
|
407
|
+
column-gap: ${columnGap}px;
|
|
408
|
+
column-fill: auto;
|
|
409
|
+
|
|
410
|
+
padding: ${paddingY}px 0;
|
|
411
|
+
box-sizing: border-box;
|
|
412
|
+
|
|
413
|
+
font-size: ${config.fontSize}px;
|
|
414
|
+
line-height: ${lineHeightRatio};
|
|
415
|
+
text-align: justify;
|
|
416
|
+
|
|
417
|
+
transform: translateX(0);
|
|
418
|
+
transition: none;
|
|
414
419
|
}
|
|
415
|
-
|
|
416
|
-
/*
|
|
417
|
-
p { margin: 0 0 0.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
+
|
|
421
|
+
/* 段落间距微调:更紧凑 */
|
|
422
|
+
p { margin: 0 0 0.6em 0; text-indent: 2em; }
|
|
423
|
+
/* 防止最后一段下边距导致额外分页 */
|
|
424
|
+
p:last-child { margin-bottom: 0; }
|
|
425
|
+
|
|
426
|
+
img { max-width: 100%; height: auto; display: block; margin: 10px auto; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); }
|
|
427
|
+
h1, h2, h3 { font-size: 1.1em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; break-after: avoid; break-inside: avoid; }
|
|
420
428
|
</style>
|
|
421
429
|
</head>
|
|
422
430
|
<body>
|
|
423
|
-
<div id="source-container"></div>
|
|
424
|
-
|
|
425
|
-
<!-- 使用 CSS Columns 进行分页布局 -->
|
|
426
|
-
<div id="content-wrapper">
|
|
427
|
-
<div class="inner-content">
|
|
428
|
-
${content}
|
|
429
|
-
</div>
|
|
430
|
-
</div>
|
|
431
|
-
|
|
432
|
-
<!-- 覆盖层:页眉页脚,截图时通过 JS 更新 -->
|
|
433
431
|
<div class="fixed-header">
|
|
434
432
|
<span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span>
|
|
435
433
|
<span>${info.UserName}</span>
|
|
436
434
|
</div>
|
|
435
|
+
|
|
436
|
+
<div id="viewport">
|
|
437
|
+
<div id="content-scroller">
|
|
438
|
+
${content}
|
|
439
|
+
</div>
|
|
440
|
+
</div>
|
|
441
|
+
|
|
437
442
|
<div class="fixed-footer" id="page-indicator">- 1 -</div>
|
|
438
443
|
</body></html>`;
|
|
439
444
|
const page = await ctx.puppeteer.page();
|
|
@@ -441,16 +446,17 @@ function apply(ctx, config) {
|
|
|
441
446
|
await injectCookies(page);
|
|
442
447
|
await page.setContent(html);
|
|
443
448
|
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
|
|
444
|
-
const scrollWidth = await page.$eval("#content-
|
|
445
|
-
const
|
|
449
|
+
const scrollWidth = await page.$eval("#content-scroller", (el) => el.scrollWidth);
|
|
450
|
+
const step = contentWidth + columnGap;
|
|
451
|
+
const totalPages = Math.floor((scrollWidth + columnGap - 10) / step) + 1;
|
|
452
|
+
const finalPages = Math.max(1, totalPages);
|
|
446
453
|
const imgs = [];
|
|
447
|
-
for (let i = 0; i <
|
|
448
|
-
await page.evaluate((
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
await page.evaluate((curr, total) => {
|
|
454
|
+
for (let i = 0; i < finalPages; i++) {
|
|
455
|
+
await page.evaluate((idx, stepPx, curr, total) => {
|
|
456
|
+
const offset = -(idx * stepPx);
|
|
457
|
+
document.getElementById("content-scroller").style.transform = `translateX(${offset}px)`;
|
|
452
458
|
document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
|
|
453
|
-
}, i + 1,
|
|
459
|
+
}, i, step, i + 1, finalPages);
|
|
454
460
|
const img = await page.screenshot({ type: "jpeg", quality: 80 });
|
|
455
461
|
imgs.push(img);
|
|
456
462
|
}
|