koishi-plugin-fimtale-api 1.0.2 → 1.0.4

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 +62 -36
  2. 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
+ // 渲染配置 (标准手机比例)
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)")
@@ -166,7 +167,6 @@ function apply(ctx, config) {
166
167
  const t = c.textContent?.trim();
167
168
  if (t && !["连载中", "已完结", "已弃坑"].includes(t) && !t.includes("展开")) tags.push(t);
168
169
  });
169
- const status = Array.from(card.querySelectorAll(".chip")).find((c) => ["连载中", "已完结", "已弃坑"].includes(c.textContent?.trim() || ""))?.textContent?.trim() || "";
170
170
  const stats = { views: "0", comments: "0", likes: "0", words: "0" };
171
171
  card.querySelectorAll(".card-action > div span[title]").forEach((s) => {
172
172
  const t = s.getAttribute("title") || "";
@@ -176,6 +176,7 @@ function apply(ctx, config) {
176
176
  if (t.includes("评论")) stats.comments = v;
177
177
  });
178
178
  stats.likes = card.querySelector(".left.green-text")?.textContent?.replace(/[^0-9]/g, "") || "0";
179
+ const status = Array.from(card.querySelectorAll(".chip")).find((c) => ["连载中", "已完结", "已弃坑"].includes(c.textContent?.trim() || ""))?.textContent?.trim() || "";
179
180
  let updateTime = "";
180
181
  const timeTxt = card.querySelector('div[style*="margin: 3px 0;"] span.grey-text')?.textContent || "";
181
182
  const dateMatch = timeTxt.match(/(\d{4}\s*年\s*\d{1,2}\s*月\s*\d{1,2}\s*日)/) || timeTxt.match(/(\d+\s*(?:小时|分钟|天)前)/) || timeTxt.match(/(\d{1,2}\s*月\s*\d{1,2}\s*日)/);
@@ -193,9 +194,10 @@ function apply(ctx, config) {
193
194
  const renderCard = /* @__PURE__ */ __name(async (info, parent) => {
194
195
  const isChapter = info.IsChapter || !!parent && parent.ID !== info.ID;
195
196
  const displayTitle = isChapter && parent ? parent.Title : info.Title;
196
- let displayCover = null;
197
- if (isChapter && parent) displayCover = parent.Background || extractImage(parent.Content);
198
- if (!displayCover) displayCover = info.Background || extractImage(info.Content);
197
+ let displayCover = info.Background || extractImage(info.Content);
198
+ if (!displayCover && isChapter && parent) {
199
+ displayCover = parent.Background || extractImage(parent.Content);
200
+ }
199
201
  const displayTagsObj = isChapter && parent ? parent.Tags : info.Tags;
200
202
  const subTitle = isChapter ? info.Title : null;
201
203
  const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
@@ -207,15 +209,22 @@ function apply(ctx, config) {
207
209
  if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
208
210
  if (displayTagsObj?.Rating && displayTagsObj.Rating !== "E") tagsArr.push(displayTagsObj.Rating);
209
211
  if (displayTagsObj?.OtherTags) tagsArr.push(...displayTagsObj.OtherTags);
212
+ const likes = isChapter && parent ? parent.Upvotes || 0 : info.Upvotes || 0;
210
213
  const html = `<!DOCTYPE html><html><head><style>
211
214
  body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
212
215
  .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; }
213
216
  .cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
214
217
  .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; }
215
218
  .info { flex: 1; padding: 24px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
219
+
220
+ /* 头部区域优化:增加下划线和间距 */
221
+ .header-group { flex-shrink: 0; margin-bottom: 16px; border-bottom: 1px dashed #f0f0f0; padding-bottom: 12px; }
216
222
  .title { font-size: 22px; font-weight: 700; color: #333; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 4px; }
217
223
  .subtitle { font-size: 15px; color: #555; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-left: 10px; border-left: 3px solid #e91e63; margin-top: 4px; }
218
- .author { font-size: 13px; color: #888; margin-top: 6px; font-weight: 400; }
224
+
225
+ /* 作者栏优化:增加顶部间距,避免紧贴标签 */
226
+ .author { font-size: 14px; color: #777; margin-top: 10px; font-weight: 400; display:flex; align-items:center; }
227
+
219
228
  .tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; flex-shrink: 0; }
220
229
  .tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
221
230
  .summary-box { flex: 1; position: relative; overflow: hidden; min-height: 0; }
@@ -230,15 +239,18 @@ function apply(ctx, config) {
230
239
  <div class="summary-box"><div class="summary">${summary}</div></div>
231
240
  <div class="footer">
232
241
  <span class="stat"><b style="color:#009688">热度</b>${info.Views || 0}</span><span class="stat"><b style="color:#673ab7">评论</b>${info.Comments || 0}</span>
233
- <span class="stat"><b style="color:#4caf50">赞</b>${info.Upvotes || 0}</span><span class="stat"><b style="color:#795548">字数</b>${info.WordCount || 0}</span>
242
+ <span class="stat"><b style="color:#4caf50">赞</b>${likes}</span><span class="stat"><b style="color:#795548">字数</b>${info.WordCount || 0}</span>
234
243
  </div></div></div></body></html>`;
235
244
  const page = await ctx.puppeteer.page();
236
- await injectCookies(page);
237
- await page.setContent(html);
238
- await page.setViewport({ width: 660, height: 480, deviceScaleFactor: 2 });
239
- const img = await page.$(".card").then((e) => e.screenshot({ type: "png" }));
240
- await page.close();
241
- return img;
245
+ try {
246
+ await injectCookies(page);
247
+ await page.setContent(html);
248
+ await page.setViewport({ width: 660, height: 480, deviceScaleFactor: 3 });
249
+ const img = await page.$(".card").then((e) => e.screenshot({ type: "jpeg", quality: 100 }));
250
+ return img;
251
+ } finally {
252
+ await page.close();
253
+ }
242
254
  }, "renderCard");
243
255
  const renderSearchResults = /* @__PURE__ */ __name(async (keyword, results) => {
244
256
  const html = `<!DOCTYPE html><html><head><style>
@@ -246,6 +258,7 @@ function apply(ctx, config) {
246
258
  .container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 10px; }
247
259
  .header { background: #fafafa; padding: 15px 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
248
260
  .header-title { font-size: 16px; font-weight: bold; color: #333; }
261
+ .list { padding: 0; }
249
262
  .item { display: flex; padding: 15px; border-bottom: 1px solid #f5f5f5; height: 110px; align-items: flex-start; }
250
263
  .cover-box { width: 75px; height: 100%; border-radius: 6px; overflow: hidden; flex-shrink: 0; margin-right: 15px; background: #eee; }
251
264
  .cover-img { width: 100%; height: 100%; object-fit: cover; }
@@ -270,11 +283,14 @@ function apply(ctx, config) {
270
283
  }).join("")}
271
284
  </div></div></body></html>`;
272
285
  const page = await ctx.puppeteer.page();
273
- await page.setContent(html);
274
- await page.setViewport({ width: 550, height: 800, deviceScaleFactor: 2 });
275
- const img = await page.$(".container").then((e) => e.screenshot({ type: "png" }));
276
- await page.close();
277
- return img;
286
+ try {
287
+ await page.setContent(html);
288
+ await page.setViewport({ width: 550, height: 800, deviceScaleFactor: 3 });
289
+ const img = await page.$(".container").then((e) => e.screenshot({ type: "jpeg", quality: 100 }));
290
+ return img;
291
+ } finally {
292
+ await page.close();
293
+ }
278
294
  }, "renderSearchResults");
279
295
  const renderReadPages = /* @__PURE__ */ __name(async (info) => {
280
296
  const content = cleanContent(info.Content);
@@ -301,19 +317,16 @@ function apply(ctx, config) {
301
317
  column-width: ${contentWidth}px; column-gap: ${columnGap}px; column-fill: auto;
302
318
  padding: ${paddingY}px 0; box-sizing: border-box;
303
319
  font-size: ${config.fontSize}px; line-height: ${lineHeightRatio};
304
- text-align: left; /* 修正:左对齐防止空格拉伸 */
320
+ text-align: left; /* 关键:左对齐,解决长空格 */
305
321
  transform: translateX(0); transition: none;
306
322
  }
307
323
 
308
- /* 基础文本 */
309
324
  p, div { margin: 0 0 0.2em 0; text-indent: 2em; word-wrap: break-word; overflow-wrap: break-word; }
310
325
 
311
- /* 辅助类 */
312
326
  .align-center { text-align: center !important; text-align-last: center !important; text-indent: 0 !important; margin: 0.8em 0; font-weight: bold; color: #5d4037; }
313
327
  .align-right { text-align: right !important; text-indent: 0 !important; margin-top: 0.5em; color: #666; font-style: italic; }
314
328
  .no-indent { text-indent: 0 !important; }
315
329
 
316
- /* 富文本支持 */
317
330
  blockquote { margin: 1em 0.5em; padding-left: 1em; border-left: 4px solid #d7ccc8; color: #666; }
318
331
  blockquote p { text-indent: 0; margin: 0.3em 0; }
319
332
 
@@ -337,11 +350,9 @@ function apply(ctx, config) {
337
350
 
338
351
  a { color: #0277bd; text-decoration: none; }
339
352
 
340
- /* 图片 (使用 figure 替代 div) */
341
353
  figure.img-box { display: flex; justify-content: center; align-items: center; margin: 0.5em 0; width: 100%; }
342
354
  img { max-width: 100%; height: auto; display: block; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); }
343
355
 
344
- /* 标题 */
345
356
  h1, h2, h3 { font-size: 1.1em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; text-align: center; text-align-last: center; break-after: avoid; }
346
357
 
347
358
  strong, b { font-weight: 900; color: #3e2723; }
@@ -356,7 +367,7 @@ function apply(ctx, config) {
356
367
  try {
357
368
  await injectCookies(page);
358
369
  await page.setContent(html);
359
- await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
370
+ await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 3 });
360
371
  await page.evaluate(async () => {
361
372
  await document.fonts.ready;
362
373
  await new Promise((resolve) => {
@@ -388,7 +399,7 @@ function apply(ctx, config) {
388
399
  document.getElementById("content-scroller").style.transform = `translateX(${offset}px)`;
389
400
  document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
390
401
  }, i, step, i + 1, finalPages);
391
- imgs.push(await page.screenshot({ type: "jpeg", quality: 80 }));
402
+ imgs.push(await page.screenshot({ type: "jpeg", quality: 100 }));
392
403
  }
393
404
  return imgs;
394
405
  } finally {
@@ -399,7 +410,7 @@ function apply(ctx, config) {
399
410
  if (!threadId) return "请输入ID";
400
411
  const res = await fetchThread(threadId);
401
412
  if (!res.valid) return `[错误] ${res.msg}`;
402
- return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
413
+ return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
403
414
  });
404
415
  ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
405
416
  if (!threadId) return "请输入ID";
@@ -408,10 +419,12 @@ function apply(ctx, config) {
408
419
  await session.send(`[加载中] ${res.data.Title}...`);
409
420
  try {
410
421
  const cardImg = await renderCard(res.data, res.parent);
411
- await session.send(import_koishi.h.image(cardImg, "image/png"));
422
+ await session.send(import_koishi.h.image(cardImg, "image/jpeg"));
412
423
  const pages = await renderReadPages(res.data);
413
424
  const nodes = pages.map((buf) => (0, import_koishi.h)("message", import_koishi.h.image(buf, "image/jpeg")));
414
425
  const navs = [];
426
+ const mainId = res.parent ? res.parent.ID : res.data.ID;
427
+ navs.push(`[目录] /ft.info ${mainId}`);
415
428
  if (res.menu?.length) {
416
429
  const idx = res.menu.findIndex((m) => m.ID.toString() === threadId);
417
430
  if (idx > 0) navs.push(`[上一章] /ft.read ${res.menu[idx - 1].ID}`);
@@ -429,7 +442,7 @@ function apply(ctx, config) {
429
442
  if (!id) return "[错误] 获取失败";
430
443
  const res = await fetchThread(id);
431
444
  if (!res.valid) return `[错误] ID:${id} 读取失败`;
432
- await session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
445
+ await session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
433
446
  return `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
434
447
  });
435
448
  ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
@@ -437,7 +450,7 @@ function apply(ctx, config) {
437
450
  await session.send("[加载中] 搜索中...");
438
451
  const results = await searchThreads(keyword);
439
452
  if (!results.length) return "未找到结果。";
440
- await session.send(import_koishi.h.image(await renderSearchResults(keyword, results), "image/png"));
453
+ await session.send(import_koishi.h.image(await renderSearchResults(keyword, results), "image/jpeg"));
441
454
  const exampleId = results[0]?.id || "12345";
442
455
  return `Tip: 发送 /ft.read [ID] 阅读 (例: /ft.read ${exampleId})`;
443
456
  });
@@ -449,7 +462,7 @@ function apply(ctx, config) {
449
462
  if (!res.valid) return "帖子不存在";
450
463
  await ctx.database.create("fimtale_subs", { cid: session.cid, threadId, lastCount: res.data.Comments, lastCheck: Date.now() });
451
464
  await session.send("[成功] 订阅成功");
452
- return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
465
+ return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
453
466
  });
454
467
  ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
455
468
  const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
@@ -457,14 +470,27 @@ function apply(ctx, config) {
457
470
  });
458
471
  ctx.middleware(async (session, next) => {
459
472
  if (!config.autoParseLink) return next();
460
- const match = session.content.match(/fimtale\.com\/t\/(\d+)/);
461
- if (match && match[1] && session.userId !== session.selfId) {
462
- const res = await fetchThread(match[1]);
463
- if (res.valid) {
464
- session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
473
+ const matches = [...session.content.matchAll(/fimtale\.com\/t\/(\d+)/g)];
474
+ if (matches.length === 0) return next();
475
+ const uniqueIds = [...new Set(matches.map((m) => m[1]))];
476
+ if (session.userId === session.selfId) return next();
477
+ const messageNodes = [];
478
+ for (const id of uniqueIds) {
479
+ try {
480
+ const res = await fetchThread(id);
481
+ if (res.valid) {
482
+ const img = await renderCard(res.data, res.parent);
483
+ messageNodes.push((0, import_koishi.h)("message", import_koishi.h.image(img, "image/jpeg")));
484
+ }
485
+ } catch (e) {
465
486
  }
466
487
  }
467
- return next();
488
+ if (messageNodes.length === 0) return next();
489
+ if (messageNodes.length === 1) {
490
+ return session.send(messageNodes[0].children);
491
+ } else {
492
+ return session.send((0, import_koishi.h)("message", { forward: true }, messageNodes));
493
+ }
468
494
  });
469
495
  ctx.setInterval(async () => {
470
496
  const subs = await ctx.database.get("fimtale_subs", {});
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-fimtale-api",
3
3
  "description": "Koishi插件,从fimtale搜索/订阅/随机获取小说/解析链接等",
4
- "version": "1.0.2",
4
+ "version": "1.0.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [