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.
Files changed (2) hide show
  1. package/lib/index.js +98 -92
  2. 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
- // 渲染配置 (iPhone 12/13 标准)
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(/&nbsp;/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="[^"]*"/gi, "").replace(/<p[^>]*>\s*(&nbsp;|<br\s*\/?>|\s)*\s*<\/p>/gi, "").replace(/(<br\s*\/?>\s*){2,}/gi, "<br>").trim();
81
+ return html.replace(/style\s*=\s*['"][^'"]*['"]/gi, "").replace(/<p[^>]*>\s*(?:<br\s*\/?>|&nbsp;|&#160;|\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*\/?>|&nbsp;|\s)+$/gi, "").trim();
87
82
  }, "cleanContent");
88
- const fontStack = '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", "WenQuanYi Micro Hei", Arial, sans-serif';
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 > 110) summary = summary.substring(0, 110) + "...";
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; justify-content: space-between; overflow: hidden; }
236
+ .info { flex: 1; padding: 24px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
242
237
 
243
- .header-group { display: flex; flex-direction: column; flex-shrink: 0; }
238
+ .header-group { flex-shrink: 0; margin-bottom: 10px; }
244
239
  .title {
245
- font-size: 22px; font-weight: 700; color: #333; line-height: 1.35;
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; margin-top: 6px; font-weight: 500;
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: 8px; font-weight: 400; }
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 0; max-height: 56px; overflow: hidden; flex-shrink: 0;}
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: 2px; /* 底部缓冲 */
260
+ padding-bottom: 3px;
269
261
  }
270
262
 
271
- .footer { border-top: 1px solid #eee; padding-top: 14px; display: flex; justify-content: space-between; font-size: 12px; color: #888; margin-top: 15px; flex-shrink: 0;}
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
- #source-container { display: none; }
374
- .page {
375
- width: ${config.deviceWidth}px; height: ${config.deviceHeight}px;
376
- /* 增加 padding,减少每页内容量,看起来更舒适 */
377
- padding: 40px 30px; box-sizing: border-box;
378
- position: relative; background: #f6f4ec; overflow: hidden;
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
- /* 自定义 CSS Column 容器 */
384
- #content-wrapper {
385
- width: ${config.deviceWidth}px; height: ${config.deviceHeight}px;
386
- column-width: ${config.deviceWidth}px; column-gap: 0; column-fill: auto;
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
- .inner-content {
392
- padding: 50px 25px 40px 25px; /* 避让页眉页脚 */
393
- box-sizing: border-box;
394
- height: 100%;
395
- font-size: ${config.fontSize}px;
396
- line-height: 1.8;
397
- text-align: justify;
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
- /* 固定页眉页脚 (Overlay) */
401
- .fixed-header {
402
- position: absolute; top: 0; left: 0; width: 100%; height: 40px;
403
- background: #f6f4ec; z-index: 10;
404
- border-bottom: 1px solid #d7ccc8;
405
- display: flex; align-items: center; justify-content: space-between;
406
- padding: 0 25px; box-sizing: border-box;
407
- font-size: 12px; color: #8d6e63; font-weight: bold; font-family: ${fontStack};
408
- }
409
- .fixed-footer {
410
- position: absolute; bottom: 0; left: 0; width: 100%; height: 30px;
411
- background: #f6f4ec; z-index: 10;
412
- display: flex; align-items: center; justify-content: center;
413
- font-size: 12px; color: #aaa; font-family: ${fontStack};
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.8em 0; text-indent: 2em; }
418
- 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; }
419
- h1, h2, h3 { font-size: 1.1em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; break-after: avoid; }
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-wrapper", (el) => el.scrollWidth);
445
- const totalPages = Math.ceil(scrollWidth / config.deviceWidth);
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 < totalPages; i++) {
448
- await page.evaluate((x) => {
449
- document.getElementById("content-wrapper").scrollTo(x, 0);
450
- }, i * config.deviceWidth);
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, totalPages);
459
+ }, i, step, i + 1, finalPages);
454
460
  const img = await page.screenshot({ type: "jpeg", quality: 80 });
455
461
  imgs.push(img);
456
462
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-fimtale-api",
3
3
  "description": "Koishi插件,从fimtale搜索/订阅/随机获取小说/解析链接等",
4
- "version": "0.0.95",
4
+ "version": "0.0.99",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [