koishi-plugin-fimtale-api 0.0.7 → 0.0.9
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 +100 -75
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -47,6 +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
|
+
// 渲染配置 (iPhone 12/13/14 标准)
|
|
50
51
|
deviceWidth: import_koishi.Schema.number().default(390).description("阅读器渲染宽度(px)"),
|
|
51
52
|
deviceHeight: import_koishi.Schema.number().default(844).description("阅读器渲染高度(px)"),
|
|
52
53
|
fontSize: import_koishi.Schema.number().default(20).description("正文字号(px)")
|
|
@@ -61,7 +62,7 @@ function apply(ctx, config) {
|
|
|
61
62
|
}, { primary: "id", autoInc: true });
|
|
62
63
|
const sleep = /* @__PURE__ */ __name((ms) => new Promise((resolve) => setTimeout(resolve, ms)), "sleep");
|
|
63
64
|
const formatDate = /* @__PURE__ */ __name((timestamp) => {
|
|
64
|
-
if (!timestamp) return "
|
|
65
|
+
if (!timestamp) return "未知日期";
|
|
65
66
|
const date = new Date(timestamp * 1e3);
|
|
66
67
|
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
|
67
68
|
}, "formatDate");
|
|
@@ -84,7 +85,8 @@ function apply(ctx, config) {
|
|
|
84
85
|
if (!html) return "";
|
|
85
86
|
return html.replace(/<p>\s* \s*<\/p>/gi, "").replace(/<p>\s*<br\s*\/?>\s*<\/p>/gi, "").replace(/<p>\s*<\/p>/gi, "").replace(/(<br\s*\/?>){2,}/gi, "<br>").replace(/margin-bottom:\s*\d+px/gi, "");
|
|
86
87
|
}, "cleanContent");
|
|
87
|
-
const fontStack = '"
|
|
88
|
+
const fontStack = '"Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif';
|
|
89
|
+
const fontSerif = '"Noto Serif SC", "Source Han Serif SC", "SimSun", serif';
|
|
88
90
|
const injectCookies = /* @__PURE__ */ __name(async (page) => {
|
|
89
91
|
if (!config.cookies) return;
|
|
90
92
|
const cookies = config.cookies.split(";").map((pair) => {
|
|
@@ -99,7 +101,7 @@ function apply(ctx, config) {
|
|
|
99
101
|
const url = `${config.apiUrl}/t/${threadId}`;
|
|
100
102
|
const params = { APIKey: config.apiKey, APIPass: config.apiPass };
|
|
101
103
|
const res = await ctx.http.get(url, { params });
|
|
102
|
-
if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API
|
|
104
|
+
if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API 返回错误" };
|
|
103
105
|
return {
|
|
104
106
|
valid: true,
|
|
105
107
|
data: res.TopicInfo,
|
|
@@ -107,7 +109,7 @@ function apply(ctx, config) {
|
|
|
107
109
|
menu: res.Menu || []
|
|
108
110
|
};
|
|
109
111
|
} catch (e) {
|
|
110
|
-
return { valid: false, msg: "
|
|
112
|
+
return { valid: false, msg: "网络请求失败" };
|
|
111
113
|
}
|
|
112
114
|
}, "fetchThread");
|
|
113
115
|
const fetchRandomId = /* @__PURE__ */ __name(async () => {
|
|
@@ -147,7 +149,7 @@ function apply(ctx, config) {
|
|
|
147
149
|
const titleEl = card.querySelector(".card-title");
|
|
148
150
|
if (titleEl) title = titleEl.textContent?.trim() || "";
|
|
149
151
|
else title = link.textContent?.trim() || "";
|
|
150
|
-
let author = "
|
|
152
|
+
let author = "未知";
|
|
151
153
|
const authorEl = card.querySelector('a[href^="/u/"] span.grey-text');
|
|
152
154
|
if (authorEl) author = authorEl.textContent?.trim() || "";
|
|
153
155
|
let cover = void 0;
|
|
@@ -218,7 +220,7 @@ function apply(ctx, config) {
|
|
|
218
220
|
const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
|
|
219
221
|
let summary = stripHtml(info.Content);
|
|
220
222
|
if (summary.length < 10 && parent && isChapter) summary = stripHtml(parent.Content);
|
|
221
|
-
if (summary.length >
|
|
223
|
+
if (summary.length > 110) summary = summary.substring(0, 110) + "...";
|
|
222
224
|
if (!summary) summary = "暂无简介";
|
|
223
225
|
const tagsArr = [];
|
|
224
226
|
if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
|
|
@@ -236,7 +238,15 @@ function apply(ctx, config) {
|
|
|
236
238
|
.card { width: 620px; height: 340px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
|
|
237
239
|
.cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
|
|
238
240
|
.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; }
|
|
239
|
-
|
|
241
|
+
|
|
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; }
|
|
240
250
|
|
|
241
251
|
.title {
|
|
242
252
|
font-size: 22px; font-weight: 700; color: #333; line-height: 1.35;
|
|
@@ -247,24 +257,31 @@ function apply(ctx, config) {
|
|
|
247
257
|
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
248
258
|
padding-left: 10px; border-left: 3px solid #e91e63;
|
|
249
259
|
}
|
|
250
|
-
|
|
251
260
|
.author { font-size: 13px; color: #888; margin-top: 8px; font-weight: 400; }
|
|
252
261
|
|
|
253
|
-
|
|
262
|
+
/* 标签栏 flex-shrink 设为 0,保证标签不被压缩 */
|
|
263
|
+
.tags {
|
|
264
|
+
display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0;
|
|
265
|
+
max-height: 56px; overflow: hidden; flex-shrink: 0;
|
|
266
|
+
}
|
|
254
267
|
.tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
|
|
255
268
|
.tag-imp { background: #e3f2fd; color: #1565c0; }
|
|
256
269
|
|
|
270
|
+
/* 简介占据剩余空间 */
|
|
257
271
|
.summary-box {
|
|
258
|
-
flex: 1;
|
|
272
|
+
flex: 1; position: relative; margin-top: 4px; overflow: hidden;
|
|
259
273
|
}
|
|
260
274
|
.summary {
|
|
261
275
|
font-size: 13px; color: #666; line-height: 1.6;
|
|
262
|
-
/* 精确控制行数和高度防止截断 */
|
|
263
|
-
max-height: 6.4em;
|
|
264
276
|
display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
|
|
265
277
|
}
|
|
266
278
|
|
|
267
|
-
|
|
279
|
+
/* 底部数据固定高度,防止被挤出 */
|
|
280
|
+
.footer {
|
|
281
|
+
border-top: 1px solid #eee; padding-top: 14px;
|
|
282
|
+
display: flex; justify-content: space-between;
|
|
283
|
+
font-size: 12px; color: #888; margin-top: 15px; flex-shrink: 0;
|
|
284
|
+
}
|
|
268
285
|
.stat b { color: #555; font-weight: bold; margin-right: 2px;}
|
|
269
286
|
</style>
|
|
270
287
|
</head>
|
|
@@ -365,81 +382,89 @@ function apply(ctx, config) {
|
|
|
365
382
|
<html>
|
|
366
383
|
<head>
|
|
367
384
|
<style>
|
|
368
|
-
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
385
|
+
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontSerif}; }
|
|
386
|
+
|
|
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;
|
|
375
411
|
}
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
412
|
+
|
|
413
|
+
/* 固定位置的页眉页脚 (overlay) */
|
|
414
|
+
.fixed-header {
|
|
415
|
+
position: fixed; top: 0; left: 0; width: 100%; height: 40px;
|
|
416
|
+
background: #f6f4ec; /* 遮挡背景 */
|
|
417
|
+
z-index: 10;
|
|
418
|
+
border-bottom: 1px solid #d7ccc8;
|
|
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};
|
|
380
422
|
}
|
|
381
|
-
.
|
|
382
|
-
|
|
383
|
-
|
|
423
|
+
.fixed-footer {
|
|
424
|
+
position: fixed; bottom: 0; left: 0; width: 100%; height: 30px;
|
|
425
|
+
background: #f6f4ec;
|
|
426
|
+
z-index: 10;
|
|
427
|
+
display: flex; align-items: center; justify-content: center;
|
|
428
|
+
font-size: 12px; color: #aaa; font-family: ${fontStack};
|
|
384
429
|
}
|
|
385
|
-
|
|
386
|
-
p { margin: 0 0 0.
|
|
387
|
-
|
|
388
|
-
|
|
430
|
+
|
|
431
|
+
p { margin: 0 0 0.8em 0; text-indent: 2em; }
|
|
432
|
+
/* 防止图片被切分 */
|
|
433
|
+
img { max-width: 100%; height: auto; display: block; margin: 15px auto; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); break-inside: avoid; }
|
|
434
|
+
h1, h2, h3 { font-size: 1.2em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; break-after: avoid; }
|
|
389
435
|
</style>
|
|
390
436
|
</head>
|
|
391
437
|
<body>
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
const header = document.createElement('div'); header.className = 'page-header';
|
|
405
|
-
header.innerHTML = \`<span>\${title.substring(0, 12) + (title.length>12?'...':'')}</span><span>\${author}</span>\`;
|
|
406
|
-
page.appendChild(header);
|
|
407
|
-
const content = document.createElement('div'); content.className = 'page-content';
|
|
408
|
-
page.appendChild(content);
|
|
409
|
-
const footer = document.createElement('div'); footer.className = 'page-footer';
|
|
410
|
-
footer.id = 'footer-' + pageIndex;
|
|
411
|
-
page.appendChild(footer);
|
|
412
|
-
output.appendChild(page);
|
|
413
|
-
currentPageContent = content;
|
|
414
|
-
return page;
|
|
415
|
-
}
|
|
416
|
-
createNewPage();
|
|
417
|
-
const children = Array.from(source.children);
|
|
418
|
-
for (const child of children) {
|
|
419
|
-
if (child.className === 'meta-info') continue;
|
|
420
|
-
currentPageContent.appendChild(child.cloneNode(true));
|
|
421
|
-
if (currentPageContent.scrollHeight > currentPageContent.clientHeight) {
|
|
422
|
-
currentPageContent.removeChild(currentPageContent.lastChild);
|
|
423
|
-
pageIndex++;
|
|
424
|
-
createNewPage();
|
|
425
|
-
currentPageContent.appendChild(child.cloneNode(true));
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
|
-
for(let i=1; i<=pageIndex; i++) document.getElementById('footer-'+i).innerText = \`- \${i} / \${pageIndex} -\`;
|
|
429
|
-
return pageIndex;
|
|
430
|
-
}
|
|
431
|
-
</script>
|
|
438
|
+
<div class="fixed-header">
|
|
439
|
+
<span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span>
|
|
440
|
+
<span>${info.UserName}</span>
|
|
441
|
+
</div>
|
|
442
|
+
|
|
443
|
+
<div id="content-wrapper">
|
|
444
|
+
<div class="inner-content">
|
|
445
|
+
${content}
|
|
446
|
+
</div>
|
|
447
|
+
</div>
|
|
448
|
+
|
|
449
|
+
<div class="fixed-footer" id="page-indicator">- 1 -</div>
|
|
432
450
|
</body></html>`;
|
|
433
451
|
const page = await ctx.puppeteer.page();
|
|
434
452
|
try {
|
|
435
453
|
await injectCookies(page);
|
|
436
454
|
await page.setContent(html);
|
|
437
455
|
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
|
|
438
|
-
await page
|
|
456
|
+
const scrollWidth = await page.$eval("#content-wrapper", (el) => el.scrollWidth);
|
|
457
|
+
const totalPages = Math.ceil(scrollWidth / config.deviceWidth);
|
|
439
458
|
const imgs = [];
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
459
|
+
for (let i = 0; i < totalPages; i++) {
|
|
460
|
+
await page.evaluate((x) => {
|
|
461
|
+
document.getElementById("content-wrapper").scrollLeft = x;
|
|
462
|
+
}, i * config.deviceWidth);
|
|
463
|
+
await page.evaluate((curr, total) => {
|
|
464
|
+
document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
|
|
465
|
+
}, i + 1, totalPages);
|
|
466
|
+
const img = await page.screenshot({ type: "jpeg", quality: 100 });
|
|
467
|
+
imgs.push(img);
|
|
443
468
|
}
|
|
444
469
|
return imgs;
|
|
445
470
|
} finally {
|