koishi-plugin-fimtale-api 1.0.3 → 1.0.5

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 +57 -43
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -166,7 +166,6 @@ function apply(ctx, config) {
166
166
  const t = c.textContent?.trim();
167
167
  if (t && !["连载中", "已完结", "已弃坑"].includes(t) && !t.includes("展开")) tags.push(t);
168
168
  });
169
- const status = Array.from(card.querySelectorAll(".chip")).find((c) => ["连载中", "已完结", "已弃坑"].includes(c.textContent?.trim() || ""))?.textContent?.trim() || "";
170
169
  const stats = { views: "0", comments: "0", likes: "0", words: "0" };
171
170
  card.querySelectorAll(".card-action > div span[title]").forEach((s) => {
172
171
  const t = s.getAttribute("title") || "";
@@ -176,6 +175,7 @@ function apply(ctx, config) {
176
175
  if (t.includes("评论")) stats.comments = v;
177
176
  });
178
177
  stats.likes = card.querySelector(".left.green-text")?.textContent?.replace(/[^0-9]/g, "") || "0";
178
+ const status = Array.from(card.querySelectorAll(".chip")).find((c) => ["连载中", "已完结", "已弃坑"].includes(c.textContent?.trim() || ""))?.textContent?.trim() || "";
179
179
  let updateTime = "";
180
180
  const timeTxt = card.querySelector('div[style*="margin: 3px 0;"] span.grey-text')?.textContent || "";
181
181
  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*日)/);
@@ -211,16 +211,23 @@ function apply(ctx, config) {
211
211
  const likes = isChapter && parent ? parent.Upvotes || 0 : info.Upvotes || 0;
212
212
  const html = `<!DOCTYPE html><html><head><style>
213
213
  body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
214
- .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; }
214
+ /* 增加卡片高度到 400px 以容纳副标题,防止内容挤压 */
215
+ .card { width: 620px; height: 400px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
215
216
  .cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
216
- .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; }
217
+ /* 更改 ID 字体 */
218
+ .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: "Impact", "Arial Black", sans-serif; letter-spacing: 1px; }
217
219
  .info { flex: 1; padding: 24px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
220
+
221
+ .header-group { flex-shrink: 0; margin-bottom: 16px; border-bottom: 1px dashed #f0f0f0; padding-bottom: 12px; }
218
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; }
219
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; }
220
- .author { font-size: 13px; color: #888; margin-top: 6px; font-weight: 400; }
224
+ .author { font-size: 14px; color: #777; margin-top: 10px; font-weight: 400; display:flex; align-items:center; }
225
+
221
226
  .tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; flex-shrink: 0; }
222
227
  .tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
223
- .summary-box { flex: 1; position: relative; overflow: hidden; min-height: 0; }
228
+
229
+ /* 增加底部边距,防止贴到底部线条 */
230
+ .summary-box { flex: 1; position: relative; overflow: hidden; min-height: 0; margin-bottom: 12px; }
224
231
  .summary { font-size: 13px; color: #666; line-height: 1.6; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; padding-bottom: 3px; }
225
232
  .footer { border-top: 1px solid #eee; padding-top: 14px; display: flex; justify-content: space-between; font-size: 12px; color: #888; margin-top: auto; flex-shrink: 0; }
226
233
  .stat b { color: #555; font-weight: bold; margin-right: 2px;}
@@ -235,12 +242,15 @@ function apply(ctx, config) {
235
242
  <span class="stat"><b style="color:#4caf50">赞</b>${likes}</span><span class="stat"><b style="color:#795548">字数</b>${info.WordCount || 0}</span>
236
243
  </div></div></div></body></html>`;
237
244
  const page = await ctx.puppeteer.page();
238
- await injectCookies(page);
239
- await page.setContent(html);
240
- await page.setViewport({ width: 660, height: 480, deviceScaleFactor: 2 });
241
- const img = await page.$(".card").then((e) => e.screenshot({ type: "png" }));
242
- await page.close();
243
- return img;
245
+ try {
246
+ await injectCookies(page);
247
+ await page.setContent(html);
248
+ await page.setViewport({ width: 660, height: 400, 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
+ }
244
254
  }, "renderCard");
245
255
  const renderSearchResults = /* @__PURE__ */ __name(async (keyword, results) => {
246
256
  const html = `<!DOCTYPE html><html><head><style>
@@ -248,6 +258,7 @@ function apply(ctx, config) {
248
258
  .container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 10px; }
249
259
  .header { background: #fafafa; padding: 15px 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
250
260
  .header-title { font-size: 16px; font-weight: bold; color: #333; }
261
+ .list { padding: 0; }
251
262
  .item { display: flex; padding: 15px; border-bottom: 1px solid #f5f5f5; height: 110px; align-items: flex-start; }
252
263
  .cover-box { width: 75px; height: 100%; border-radius: 6px; overflow: hidden; flex-shrink: 0; margin-right: 15px; background: #eee; }
253
264
  .cover-img { width: 100%; height: 100%; object-fit: cover; }
@@ -272,11 +283,14 @@ function apply(ctx, config) {
272
283
  }).join("")}
273
284
  </div></div></body></html>`;
274
285
  const page = await ctx.puppeteer.page();
275
- await page.setContent(html);
276
- await page.setViewport({ width: 550, height: 800, deviceScaleFactor: 2 });
277
- const img = await page.$(".container").then((e) => e.screenshot({ type: "png" }));
278
- await page.close();
279
- 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
+ }
280
294
  }, "renderSearchResults");
281
295
  const renderReadPages = /* @__PURE__ */ __name(async (info) => {
282
296
  const content = cleanContent(info.Content);
@@ -298,44 +312,31 @@ function apply(ctx, config) {
298
312
  .fixed-footer { position: absolute; bottom: 0; left: 0; width: 100%; height: ${footerHeight}px; display: flex; align-items: center; justify-content: center; font-size: 12px; color: #aaa; background: #f6f4ec; z-index: 5; }
299
313
  #viewport { position: absolute; top: ${marginTop}px; left: ${paddingX}px; width: ${contentWidth}px; height: ${optimalContentHeight}px; overflow: hidden; }
300
314
  #content-scroller { height: 100%; width: 100%; column-width: ${contentWidth}px; column-gap: ${columnGap}px; column-fill: auto; padding: ${paddingY}px 0; box-sizing: border-box; font-size: ${config.fontSize}px; line-height: ${lineHeightRatio}; text-align: left; transform: translateX(0); transition: none; }
301
-
302
315
  p, div { margin: 0 0 0.2em 0; text-indent: 2em; word-wrap: break-word; overflow-wrap: break-word; }
303
-
304
316
  .align-center { text-align: center !important; text-align-last: center !important; text-indent: 0 !important; margin: 0.8em 0; font-weight: bold; color: #5d4037; }
305
317
  .align-right { text-align: right !important; text-indent: 0 !important; margin-top: 0.5em; color: #666; font-style: italic; }
306
318
  .no-indent { text-indent: 0 !important; }
307
-
308
319
  blockquote { margin: 1em 0.5em; padding-left: 1em; border-left: 4px solid #d7ccc8; color: #666; }
309
320
  blockquote p { text-indent: 0; margin: 0.3em 0; }
310
-
311
321
  ul, ol { margin: 0.5em 0; padding-left: 1.5em; }
312
322
  li { margin-bottom: 0.2em; }
313
-
314
323
  hr { border: 0; height: 1px; background: #d7ccc8; margin: 1.5em 0; }
315
-
316
324
  table { width: 100%; border-collapse: collapse; margin: 1em 0; font-size: 0.9em; }
317
325
  th, td { border: 1px solid #ccc; padding: 4px; text-align: left; }
318
326
  th { background: #eee; font-weight: bold; }
319
-
320
327
  pre { background: #eee; padding: 0.5em; overflow-x: auto; border-radius: 4px; margin: 0.5em 0; }
321
328
  code { font-family: monospace; background: #f0f0f0; padding: 2px 4px; border-radius: 3px; }
322
-
323
329
  s, strike, del { text-decoration: line-through; color: #888; }
324
330
  u { text-decoration: underline; }
325
331
  sup, sub { font-size: 0.75em; line-height: 0; position: relative; vertical-align: baseline; }
326
332
  sup { top: -0.5em; }
327
333
  sub { bottom: -0.25em; }
328
-
329
334
  a { color: #0277bd; text-decoration: none; }
330
-
331
335
  figure.img-box { display: flex; justify-content: center; align-items: center; margin: 0.5em 0; width: 100%; }
332
336
  img { max-width: 100%; height: auto; display: block; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); }
333
-
334
337
  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; }
335
-
336
338
  strong, b { font-weight: 900; color: #3e2723; }
337
339
  em, i { font-style: italic; }
338
-
339
340
  p:last-child { margin-bottom: 0; }
340
341
  </style></head><body>
341
342
  <div class="fixed-header"><span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span><span>${info.UserName}</span></div>
@@ -345,7 +346,7 @@ function apply(ctx, config) {
345
346
  try {
346
347
  await injectCookies(page);
347
348
  await page.setContent(html);
348
- await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
349
+ await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 3 });
349
350
  await page.evaluate(async () => {
350
351
  await document.fonts.ready;
351
352
  await new Promise((resolve) => {
@@ -377,7 +378,7 @@ function apply(ctx, config) {
377
378
  document.getElementById("content-scroller").style.transform = `translateX(${offset}px)`;
378
379
  document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
379
380
  }, i, step, i + 1, finalPages);
380
- imgs.push(await page.screenshot({ type: "jpeg", quality: 80 }));
381
+ imgs.push(await page.screenshot({ type: "jpeg", quality: 100 }));
381
382
  }
382
383
  return imgs;
383
384
  } finally {
@@ -388,7 +389,7 @@ function apply(ctx, config) {
388
389
  if (!threadId) return "请输入ID";
389
390
  const res = await fetchThread(threadId);
390
391
  if (!res.valid) return `[错误] ${res.msg}`;
391
- return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
392
+ return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
392
393
  });
393
394
  ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
394
395
  if (!threadId) return "请输入ID";
@@ -397,12 +398,12 @@ function apply(ctx, config) {
397
398
  await session.send(`[加载中] ${res.data.Title}...`);
398
399
  try {
399
400
  const cardImg = await renderCard(res.data, res.parent);
400
- await session.send(import_koishi.h.image(cardImg, "image/png"));
401
+ await session.send(import_koishi.h.image(cardImg, "image/jpeg"));
401
402
  const pages = await renderReadPages(res.data);
402
403
  const nodes = pages.map((buf) => (0, import_koishi.h)("message", import_koishi.h.image(buf, "image/jpeg")));
403
404
  const navs = [];
404
405
  const mainId = res.parent ? res.parent.ID : res.data.ID;
405
- navs.push(`[目录] /ft.info ${mainId}`);
406
+ navs.push(`[首页] /ft.read ${mainId}`);
406
407
  if (res.menu?.length) {
407
408
  const idx = res.menu.findIndex((m) => m.ID.toString() === threadId);
408
409
  if (idx > 0) navs.push(`[上一章] /ft.read ${res.menu[idx - 1].ID}`);
@@ -420,7 +421,7 @@ function apply(ctx, config) {
420
421
  if (!id) return "[错误] 获取失败";
421
422
  const res = await fetchThread(id);
422
423
  if (!res.valid) return `[错误] ID:${id} 读取失败`;
423
- await session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
424
+ await session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
424
425
  return `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
425
426
  });
426
427
  ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
@@ -428,7 +429,7 @@ function apply(ctx, config) {
428
429
  await session.send("[加载中] 搜索中...");
429
430
  const results = await searchThreads(keyword);
430
431
  if (!results.length) return "未找到结果。";
431
- await session.send(import_koishi.h.image(await renderSearchResults(keyword, results), "image/png"));
432
+ await session.send(import_koishi.h.image(await renderSearchResults(keyword, results), "image/jpeg"));
432
433
  const exampleId = results[0]?.id || "12345";
433
434
  return `Tip: 发送 /ft.read [ID] 阅读 (例: /ft.read ${exampleId})`;
434
435
  });
@@ -440,7 +441,7 @@ function apply(ctx, config) {
440
441
  if (!res.valid) return "帖子不存在";
441
442
  await ctx.database.create("fimtale_subs", { cid: session.cid, threadId, lastCount: res.data.Comments, lastCheck: Date.now() });
442
443
  await session.send("[成功] 订阅成功");
443
- return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
444
+ return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
444
445
  });
445
446
  ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
446
447
  const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
@@ -448,14 +449,27 @@ function apply(ctx, config) {
448
449
  });
449
450
  ctx.middleware(async (session, next) => {
450
451
  if (!config.autoParseLink) return next();
451
- const match = session.content.match(/fimtale\.com\/t\/(\d+)/);
452
- if (match && match[1] && session.userId !== session.selfId) {
453
- const res = await fetchThread(match[1]);
454
- if (res.valid) {
455
- session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
452
+ const matches = [...session.content.matchAll(/fimtale\.com\/t\/(\d+)/g)];
453
+ if (matches.length === 0) return next();
454
+ const uniqueIds = [...new Set(matches.map((m) => m[1]))];
455
+ if (session.userId === session.selfId) return next();
456
+ const messageNodes = [];
457
+ for (const id of uniqueIds) {
458
+ try {
459
+ const res = await fetchThread(id);
460
+ if (res.valid) {
461
+ const img = await renderCard(res.data, res.parent);
462
+ messageNodes.push((0, import_koishi.h)("message", import_koishi.h.image(img, "image/jpeg")));
463
+ }
464
+ } catch (e) {
456
465
  }
457
466
  }
458
- return next();
467
+ if (messageNodes.length === 0) return next();
468
+ if (messageNodes.length === 1) {
469
+ return session.send(messageNodes[0].children);
470
+ } else {
471
+ return session.send((0, import_koishi.h)("message", { forward: true }, messageNodes));
472
+ }
459
473
  });
460
474
  ctx.setInterval(async () => {
461
475
  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.3",
4
+ "version": "1.0.5",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [