koishi-plugin-fimtale-api 0.0.95 → 0.0.98
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 +104 -88
- 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();
|
|
@@ -85,7 +80,7 @@ function apply(ctx, config) {
|
|
|
85
80
|
if (!html) return "";
|
|
86
81
|
return html.replace(/style="[^"]*"/gi, "").replace(/<p[^>]*>\s*( |<br\s*\/?>|\s)*\s*<\/p>/gi, "").replace(/(<br\s*\/?>\s*){2,}/gi, "<br>").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);
|
|
@@ -236,39 +231,54 @@ function apply(ctx, config) {
|
|
|
236
231
|
<style>
|
|
237
232
|
body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
|
|
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; }
|
|
234
|
+
|
|
239
235
|
.cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
|
|
240
236
|
.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; justify-content: space-between; overflow: hidden; }
|
|
242
237
|
|
|
243
|
-
.
|
|
238
|
+
.info { flex: 1; padding: 24px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
|
|
239
|
+
|
|
240
|
+
.header-group { flex-shrink: 0; margin-bottom: 10px; }
|
|
244
241
|
.title {
|
|
245
|
-
font-size: 22px; font-weight: 700; color: #333; line-height: 1.
|
|
242
|
+
font-size: 22px; font-weight: 700; color: #333; line-height: 1.4;
|
|
246
243
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
244
|
+
margin-bottom: 4px;
|
|
247
245
|
}
|
|
248
246
|
.subtitle {
|
|
249
|
-
font-size: 15px; color: #555;
|
|
247
|
+
font-size: 15px; color: #555; font-weight: 500;
|
|
250
248
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
251
249
|
padding-left: 10px; border-left: 3px solid #e91e63;
|
|
250
|
+
margin-top: 4px;
|
|
252
251
|
}
|
|
253
|
-
.author { font-size: 13px; color: #888; margin-top:
|
|
252
|
+
.author { font-size: 13px; color: #888; margin-top: 6px; font-weight: 400; }
|
|
254
253
|
|
|
255
|
-
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px
|
|
254
|
+
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; flex-shrink: 0; }
|
|
256
255
|
.tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
|
|
257
256
|
.tag-imp { background: #e3f2fd; color: #1565c0; }
|
|
258
257
|
|
|
259
|
-
/*
|
|
258
|
+
/* 修复:使用 Flex 自动填充剩余空间,并增加底部 padding 防止截断 */
|
|
260
259
|
.summary-box {
|
|
261
|
-
flex: 1;
|
|
260
|
+
flex: 1;
|
|
261
|
+
position: relative;
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
min-height: 0;
|
|
262
264
|
}
|
|
263
265
|
.summary {
|
|
264
|
-
font-size: 13px; color: #666;
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
font-size: 13px; color: #666; line-height: 1.6;
|
|
267
|
+
display: -webkit-box;
|
|
268
|
+
-webkit-line-clamp: 4;
|
|
269
|
+
-webkit-box-orient: vertical;
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
padding-bottom: 3px; /* 关键修复:防止下行字母(如g,j,p,q,y)被切掉 */
|
|
269
272
|
}
|
|
270
273
|
|
|
271
|
-
.footer {
|
|
274
|
+
.footer {
|
|
275
|
+
border-top: 1px solid #eee;
|
|
276
|
+
padding-top: 14px;
|
|
277
|
+
display: flex; justify-content: space-between;
|
|
278
|
+
font-size: 12px; color: #888;
|
|
279
|
+
margin-top: auto; /* 确保页脚始终在底部 */
|
|
280
|
+
flex-shrink: 0;
|
|
281
|
+
}
|
|
272
282
|
.stat b { color: #555; font-weight: bold; margin-right: 2px;}
|
|
273
283
|
</style>
|
|
274
284
|
</head>
|
|
@@ -281,13 +291,8 @@ function apply(ctx, config) {
|
|
|
281
291
|
${subTitle ? `<div class="subtitle">${subTitle}</div>` : ""}
|
|
282
292
|
<div class="author">@${info.UserName}</div>
|
|
283
293
|
</div>
|
|
284
|
-
|
|
285
294
|
<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
|
-
|
|
295
|
+
<div class="summary-box"><div class="summary">${summary}</div></div>
|
|
291
296
|
<div class="footer">
|
|
292
297
|
<span class="stat"><b style="color:#009688">热度</b>${views}</span>
|
|
293
298
|
<span class="stat"><b style="color:#673ab7">评论</b>${comments}</span>
|
|
@@ -364,76 +369,87 @@ function apply(ctx, config) {
|
|
|
364
369
|
}, "renderSearchResults");
|
|
365
370
|
const renderReadPages = /* @__PURE__ */ __name(async (info) => {
|
|
366
371
|
const content = cleanContent(info.Content);
|
|
372
|
+
const headerHeight = 40;
|
|
373
|
+
const footerHeight = 30;
|
|
374
|
+
const paddingX = 25;
|
|
375
|
+
const paddingY = 20;
|
|
376
|
+
const lineHeightRatio = 1.8;
|
|
377
|
+
const contentWidth = config.deviceWidth - paddingX * 2;
|
|
378
|
+
const columnGap = 40;
|
|
379
|
+
const maxContentHeight = config.deviceHeight - headerHeight - footerHeight;
|
|
380
|
+
const lineHeightPx = config.fontSize * lineHeightRatio;
|
|
381
|
+
const linesPerPage = Math.floor((maxContentHeight - paddingY * 2) / lineHeightPx);
|
|
382
|
+
const optimalContentHeight = linesPerPage * lineHeightPx + paddingY * 2;
|
|
383
|
+
const marginTop = Math.floor((maxContentHeight - optimalContentHeight) / 2) + headerHeight;
|
|
367
384
|
const html = `
|
|
368
385
|
<!DOCTYPE html>
|
|
369
386
|
<html>
|
|
370
387
|
<head>
|
|
371
388
|
<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;
|
|
389
|
+
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;}
|
|
390
|
+
|
|
391
|
+
.fixed-header {
|
|
392
|
+
position: absolute; top: 0; left: 0; width: 100%; height: ${headerHeight}px;
|
|
393
|
+
border-bottom: 1px solid #d7ccc8; box-sizing: border-box;
|
|
394
|
+
padding: 0 20px; display: flex; align-items: center; justify-content: space-between;
|
|
395
|
+
font-size: 12px; color: #8d6e63; background: #f6f4ec; z-index: 5; font-weight: bold;
|
|
381
396
|
}
|
|
382
397
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
overflow-x: hidden; overflow-y: hidden; /* 隐藏滚动条 */
|
|
398
|
+
.fixed-footer {
|
|
399
|
+
position: absolute; bottom: 0; left: 0; width: 100%; height: ${footerHeight}px;
|
|
400
|
+
display: flex; align-items: center; justify-content: center;
|
|
401
|
+
font-size: 12px; color: #aaa; background: #f6f4ec; z-index: 5;
|
|
388
402
|
}
|
|
389
403
|
|
|
390
|
-
/*
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
404
|
+
/* 视口容器:用于隐藏溢出的页面 */
|
|
405
|
+
#viewport {
|
|
406
|
+
position: absolute;
|
|
407
|
+
top: ${marginTop}px;
|
|
408
|
+
left: ${paddingX}px; /* 左边距 */
|
|
409
|
+
width: ${contentWidth}px; /* 限制视口只显示一页内容的宽度 */
|
|
410
|
+
height: ${optimalContentHeight}px;
|
|
411
|
+
overflow: hidden; /* 关键:隐藏其他页 */
|
|
398
412
|
}
|
|
399
413
|
|
|
400
|
-
/*
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
+
/* 内容长条:实际上是超宽的 */
|
|
415
|
+
#content-scroller {
|
|
416
|
+
height: 100%;
|
|
417
|
+
width: 100%; /* 这里的 100% 是相对于 viewport 的 contentWidth */
|
|
418
|
+
|
|
419
|
+
/* CSS Columns 布局 */
|
|
420
|
+
column-width: ${contentWidth}px;
|
|
421
|
+
column-gap: ${columnGap}px;
|
|
422
|
+
column-fill: auto;
|
|
423
|
+
|
|
424
|
+
padding: ${paddingY}px 0; /* 左右不需要 padding,因为 viewport 已经定了位置 */
|
|
425
|
+
box-sizing: border-box;
|
|
426
|
+
|
|
427
|
+
font-size: ${config.fontSize}px;
|
|
428
|
+
line-height: ${lineHeightRatio};
|
|
429
|
+
text-align: justify;
|
|
430
|
+
|
|
431
|
+
/* 关键:使用 transform 移动,初始位置为 0 */
|
|
432
|
+
transform: translateX(0);
|
|
433
|
+
transition: none;
|
|
414
434
|
}
|
|
415
|
-
|
|
416
|
-
/* 排版细节 */
|
|
435
|
+
|
|
417
436
|
p { margin: 0 0 0.8em 0; text-indent: 2em; }
|
|
418
|
-
img { max-width: 100%; height: auto; display: block; margin:
|
|
419
|
-
h1, h2, h3 { font-size: 1.1em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; break-after: avoid; }
|
|
437
|
+
img { max-width: 100%; height: auto; display: block; margin: 10px auto; border-radius: 6px; }
|
|
438
|
+
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
439
|
</style>
|
|
421
440
|
</head>
|
|
422
441
|
<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
442
|
<div class="fixed-header">
|
|
434
443
|
<span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span>
|
|
435
444
|
<span>${info.UserName}</span>
|
|
436
445
|
</div>
|
|
446
|
+
|
|
447
|
+
<div id="viewport">
|
|
448
|
+
<div id="content-scroller">
|
|
449
|
+
${content}
|
|
450
|
+
</div>
|
|
451
|
+
</div>
|
|
452
|
+
|
|
437
453
|
<div class="fixed-footer" id="page-indicator">- 1 -</div>
|
|
438
454
|
</body></html>`;
|
|
439
455
|
const page = await ctx.puppeteer.page();
|
|
@@ -441,16 +457,16 @@ function apply(ctx, config) {
|
|
|
441
457
|
await injectCookies(page);
|
|
442
458
|
await page.setContent(html);
|
|
443
459
|
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
|
|
444
|
-
const scrollWidth = await page.$eval("#content-
|
|
445
|
-
const
|
|
460
|
+
const scrollWidth = await page.$eval("#content-scroller", (el) => el.scrollWidth);
|
|
461
|
+
const step = contentWidth + columnGap;
|
|
462
|
+
const totalPages = Math.ceil((scrollWidth + columnGap) / step) || 1;
|
|
446
463
|
const imgs = [];
|
|
447
464
|
for (let i = 0; i < totalPages; i++) {
|
|
448
|
-
await page.evaluate((
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
await page.evaluate((curr, total) => {
|
|
465
|
+
await page.evaluate((idx, stepPx, curr, total) => {
|
|
466
|
+
const offset = -(idx * stepPx);
|
|
467
|
+
document.getElementById("content-scroller").style.transform = `translateX(${offset}px)`;
|
|
452
468
|
document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
|
|
453
|
-
}, i + 1, totalPages);
|
|
469
|
+
}, i, step, i + 1, totalPages);
|
|
454
470
|
const img = await page.screenshot({ type: "jpeg", quality: 80 });
|
|
455
471
|
imgs.push(img);
|
|
456
472
|
}
|