koishi-plugin-fimtale-api 0.0.9 → 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 +107 -103
- 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 "未知日期";
|
|
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,7 +78,7 @@ function apply(ctx, config) {
|
|
|
83
78
|
}, "generateGradient");
|
|
84
79
|
const cleanContent = /* @__PURE__ */ __name((html) => {
|
|
85
80
|
if (!html) return "";
|
|
86
|
-
return html.replace(
|
|
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
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';
|
|
@@ -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);
|
|
@@ -235,52 +230,54 @@ function apply(ctx, config) {
|
|
|
235
230
|
<head>
|
|
236
231
|
<style>
|
|
237
232
|
body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
|
|
238
|
-
.card { width: 620px; height:
|
|
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
237
|
|
|
242
|
-
.info {
|
|
243
|
-
flex: 1; padding: 24px;
|
|
244
|
-
display: flex; flex-direction: column;
|
|
245
|
-
/* 关键:内容溢出隐藏,防止撑开 */
|
|
246
|
-
overflow: hidden;
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
.header-group { display: flex; flex-direction: column; flex-shrink: 0; }
|
|
238
|
+
.info { flex: 1; padding: 24px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
|
|
250
239
|
|
|
240
|
+
.header-group { flex-shrink: 0; margin-bottom: 10px; }
|
|
251
241
|
.title {
|
|
252
|
-
font-size: 22px; font-weight: 700; color: #333; line-height: 1.
|
|
242
|
+
font-size: 22px; font-weight: 700; color: #333; line-height: 1.4;
|
|
253
243
|
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
244
|
+
margin-bottom: 4px;
|
|
254
245
|
}
|
|
255
246
|
.subtitle {
|
|
256
|
-
font-size: 15px; color: #555;
|
|
247
|
+
font-size: 15px; color: #555; font-weight: 500;
|
|
257
248
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
258
249
|
padding-left: 10px; border-left: 3px solid #e91e63;
|
|
250
|
+
margin-top: 4px;
|
|
259
251
|
}
|
|
260
|
-
.author { font-size: 13px; color: #888; margin-top:
|
|
252
|
+
.author { font-size: 13px; color: #888; margin-top: 6px; font-weight: 400; }
|
|
261
253
|
|
|
262
|
-
|
|
263
|
-
.tags {
|
|
264
|
-
display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0;
|
|
265
|
-
max-height: 56px; overflow: hidden; flex-shrink: 0;
|
|
266
|
-
}
|
|
254
|
+
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; flex-shrink: 0; }
|
|
267
255
|
.tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
|
|
268
256
|
.tag-imp { background: #e3f2fd; color: #1565c0; }
|
|
269
257
|
|
|
270
|
-
/*
|
|
258
|
+
/* 修复:使用 Flex 自动填充剩余空间,并增加底部 padding 防止截断 */
|
|
271
259
|
.summary-box {
|
|
272
|
-
flex: 1;
|
|
260
|
+
flex: 1;
|
|
261
|
+
position: relative;
|
|
262
|
+
overflow: hidden;
|
|
263
|
+
min-height: 0;
|
|
273
264
|
}
|
|
274
265
|
.summary {
|
|
275
266
|
font-size: 13px; color: #666; line-height: 1.6;
|
|
276
|
-
display: -webkit-box;
|
|
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)被切掉 */
|
|
277
272
|
}
|
|
278
273
|
|
|
279
|
-
/* 底部数据固定高度,防止被挤出 */
|
|
280
274
|
.footer {
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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;
|
|
284
281
|
}
|
|
285
282
|
.stat b { color: #555; font-weight: bold; margin-right: 2px;}
|
|
286
283
|
</style>
|
|
@@ -294,13 +291,8 @@ function apply(ctx, config) {
|
|
|
294
291
|
${subTitle ? `<div class="subtitle">${subTitle}</div>` : ""}
|
|
295
292
|
<div class="author">@${info.UserName}</div>
|
|
296
293
|
</div>
|
|
297
|
-
|
|
298
294
|
<div class="tags">${displayTags.map((t) => `<span class="tag ${["文", "译", "R"].includes(t) ? "tag-imp" : ""}">${t}</span>`).join("")}</div>
|
|
299
|
-
|
|
300
|
-
<div class="summary-box">
|
|
301
|
-
<div class="summary">${summary}</div>
|
|
302
|
-
</div>
|
|
303
|
-
|
|
295
|
+
<div class="summary-box"><div class="summary">${summary}</div></div>
|
|
304
296
|
<div class="footer">
|
|
305
297
|
<span class="stat"><b style="color:#009688">热度</b>${views}</span>
|
|
306
298
|
<span class="stat"><b style="color:#673ab7">评论</b>${comments}</span>
|
|
@@ -360,7 +352,7 @@ function apply(ctx, config) {
|
|
|
360
352
|
<div class="top-row"><div class="title">${r.title}</div><div class="id-badge">ID: ${r.id}</div></div>
|
|
361
353
|
<div class="author">By ${r.author} ${r.status ? ` · ${r.status}` : ""}</div>
|
|
362
354
|
<div class="tags">${r.tags.map((t) => `<span class="tag">${t}</span>`).join("")}</div>
|
|
363
|
-
<div class="meta-row">${stats || "
|
|
355
|
+
<div class="meta-row">${stats || "No Data"}</div>
|
|
364
356
|
</div></div>`;
|
|
365
357
|
}).join("")}
|
|
366
358
|
</div>
|
|
@@ -377,93 +369,105 @@ function apply(ctx, config) {
|
|
|
377
369
|
}, "renderSearchResults");
|
|
378
370
|
const renderReadPages = /* @__PURE__ */ __name(async (info) => {
|
|
379
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;
|
|
380
384
|
const html = `
|
|
381
385
|
<!DOCTYPE html>
|
|
382
386
|
<html>
|
|
383
387
|
<head>
|
|
384
388
|
<style>
|
|
385
|
-
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontSerif}; }
|
|
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;}
|
|
386
390
|
|
|
387
|
-
/* 页面容器:包含页眉、内容、页脚 */
|
|
388
|
-
/* 固定高度,让 CSS Column 生效 */
|
|
389
|
-
#content-wrapper {
|
|
390
|
-
width: ${config.deviceWidth}px;
|
|
391
|
-
height: ${config.deviceHeight}px;
|
|
392
|
-
column-width: ${config.deviceWidth}px;
|
|
393
|
-
column-gap: 0;
|
|
394
|
-
|
|
395
|
-
/* 强制水平滚动 */
|
|
396
|
-
overflow-y: hidden;
|
|
397
|
-
overflow-x: scroll;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
/* 每一页的实际内容区域 */
|
|
401
|
-
/* 我们需要用 padding 来模拟页眉页脚的空间 */
|
|
402
|
-
/* 上留 50px,下留 40px,左右 25px */
|
|
403
|
-
.inner-content {
|
|
404
|
-
padding: 50px 25px 40px 25px;
|
|
405
|
-
box-sizing: border-box;
|
|
406
|
-
height: 100%; /* 填满容器高度 */
|
|
407
|
-
|
|
408
|
-
font-size: ${config.fontSize}px;
|
|
409
|
-
line-height: 1.8;
|
|
410
|
-
text-align: justify;
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/* 固定位置的页眉页脚 (overlay) */
|
|
414
391
|
.fixed-header {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
display: flex; align-items: center; justify-content: space-between;
|
|
420
|
-
padding: 0 20px; box-sizing: border-box;
|
|
421
|
-
font-size: 12px; color: #8d6e63; font-weight: bold; font-family: ${fontStack};
|
|
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;
|
|
422
396
|
}
|
|
397
|
+
|
|
423
398
|
.fixed-footer {
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
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;
|
|
402
|
+
}
|
|
403
|
+
|
|
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; /* 关键:隐藏其他页 */
|
|
429
412
|
}
|
|
430
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;
|
|
434
|
+
}
|
|
435
|
+
|
|
431
436
|
p { margin: 0 0 0.8em 0; text-indent: 2em; }
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
h1, h2, h3 { font-size: 1.2em; 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; }
|
|
435
439
|
</style>
|
|
436
440
|
</head>
|
|
437
441
|
<body>
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
</div>
|
|
442
|
+
<div class="fixed-header">
|
|
443
|
+
<span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span>
|
|
444
|
+
<span>${info.UserName}</span>
|
|
445
|
+
</div>
|
|
446
|
+
|
|
447
|
+
<div id="viewport">
|
|
448
|
+
<div id="content-scroller">
|
|
449
|
+
${content}
|
|
447
450
|
</div>
|
|
451
|
+
</div>
|
|
448
452
|
|
|
449
|
-
|
|
453
|
+
<div class="fixed-footer" id="page-indicator">- 1 -</div>
|
|
450
454
|
</body></html>`;
|
|
451
455
|
const page = await ctx.puppeteer.page();
|
|
452
456
|
try {
|
|
453
457
|
await injectCookies(page);
|
|
454
458
|
await page.setContent(html);
|
|
455
459
|
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
|
|
456
|
-
const scrollWidth = await page.$eval("#content-
|
|
457
|
-
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;
|
|
458
463
|
const imgs = [];
|
|
459
464
|
for (let i = 0; i < totalPages; i++) {
|
|
460
|
-
await page.evaluate((
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
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)`;
|
|
464
468
|
document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
|
|
465
|
-
}, i + 1, totalPages);
|
|
466
|
-
const img = await page.screenshot({ type: "jpeg", quality:
|
|
469
|
+
}, i, step, i + 1, totalPages);
|
|
470
|
+
const img = await page.screenshot({ type: "jpeg", quality: 80 });
|
|
467
471
|
imgs.push(img);
|
|
468
472
|
}
|
|
469
473
|
return imgs;
|