koishi-plugin-fimtale-api 1.0.3 → 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 -27
  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*日)/);
@@ -215,9 +216,15 @@ function apply(ctx, config) {
215
216
  .cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
216
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; }
217
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; }
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
+
225
+ /* 作者栏优化:增加顶部间距,避免紧贴标签 */
226
+ .author { font-size: 14px; color: #777; margin-top: 10px; font-weight: 400; display:flex; align-items:center; }
227
+
221
228
  .tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; flex-shrink: 0; }
222
229
  .tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
223
230
  .summary-box { flex: 1; position: relative; overflow: hidden; min-height: 0; }
@@ -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: 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
+ }
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);
@@ -297,7 +311,15 @@ function apply(ctx, config) {
297
311
  .fixed-header { position: absolute; top: 0; left: 0; width: 100%; height: ${headerHeight}px; border-bottom: 1px solid #d7ccc8; box-sizing: border-box; padding: 0 20px; display: flex; align-items: center; justify-content: space-between; font-size: 12px; color: #8d6e63; background: #f6f4ec; z-index: 5; font-weight: bold; }
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
- #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; }
314
+
315
+ #content-scroller {
316
+ height: 100%; width: 100%;
317
+ column-width: ${contentWidth}px; column-gap: ${columnGap}px; column-fill: auto;
318
+ padding: ${paddingY}px 0; box-sizing: border-box;
319
+ font-size: ${config.fontSize}px; line-height: ${lineHeightRatio};
320
+ text-align: left; /* 关键:左对齐,解决长空格 */
321
+ transform: translateX(0); transition: none;
322
+ }
301
323
 
302
324
  p, div { margin: 0 0 0.2em 0; text-indent: 2em; word-wrap: break-word; overflow-wrap: break-word; }
303
325
 
@@ -345,7 +367,7 @@ function apply(ctx, config) {
345
367
  try {
346
368
  await injectCookies(page);
347
369
  await page.setContent(html);
348
- await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
370
+ await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 3 });
349
371
  await page.evaluate(async () => {
350
372
  await document.fonts.ready;
351
373
  await new Promise((resolve) => {
@@ -377,7 +399,7 @@ function apply(ctx, config) {
377
399
  document.getElementById("content-scroller").style.transform = `translateX(${offset}px)`;
378
400
  document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
379
401
  }, i, step, i + 1, finalPages);
380
- imgs.push(await page.screenshot({ type: "jpeg", quality: 80 }));
402
+ imgs.push(await page.screenshot({ type: "jpeg", quality: 100 }));
381
403
  }
382
404
  return imgs;
383
405
  } finally {
@@ -388,7 +410,7 @@ function apply(ctx, config) {
388
410
  if (!threadId) return "请输入ID";
389
411
  const res = await fetchThread(threadId);
390
412
  if (!res.valid) return `[错误] ${res.msg}`;
391
- 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"));
392
414
  });
393
415
  ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
394
416
  if (!threadId) return "请输入ID";
@@ -397,7 +419,7 @@ function apply(ctx, config) {
397
419
  await session.send(`[加载中] ${res.data.Title}...`);
398
420
  try {
399
421
  const cardImg = await renderCard(res.data, res.parent);
400
- await session.send(import_koishi.h.image(cardImg, "image/png"));
422
+ await session.send(import_koishi.h.image(cardImg, "image/jpeg"));
401
423
  const pages = await renderReadPages(res.data);
402
424
  const nodes = pages.map((buf) => (0, import_koishi.h)("message", import_koishi.h.image(buf, "image/jpeg")));
403
425
  const navs = [];
@@ -420,7 +442,7 @@ function apply(ctx, config) {
420
442
  if (!id) return "[错误] 获取失败";
421
443
  const res = await fetchThread(id);
422
444
  if (!res.valid) return `[错误] ID:${id} 读取失败`;
423
- 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"));
424
446
  return `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
425
447
  });
426
448
  ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
@@ -428,7 +450,7 @@ function apply(ctx, config) {
428
450
  await session.send("[加载中] 搜索中...");
429
451
  const results = await searchThreads(keyword);
430
452
  if (!results.length) return "未找到结果。";
431
- 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"));
432
454
  const exampleId = results[0]?.id || "12345";
433
455
  return `Tip: 发送 /ft.read [ID] 阅读 (例: /ft.read ${exampleId})`;
434
456
  });
@@ -440,7 +462,7 @@ function apply(ctx, config) {
440
462
  if (!res.valid) return "帖子不存在";
441
463
  await ctx.database.create("fimtale_subs", { cid: session.cid, threadId, lastCount: res.data.Comments, lastCheck: Date.now() });
442
464
  await session.send("[成功] 订阅成功");
443
- 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"));
444
466
  });
445
467
  ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
446
468
  const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
@@ -448,14 +470,27 @@ function apply(ctx, config) {
448
470
  });
449
471
  ctx.middleware(async (session, next) => {
450
472
  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"));
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) {
456
486
  }
457
487
  }
458
- 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
+ }
459
494
  });
460
495
  ctx.setInterval(async () => {
461
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.3",
4
+ "version": "1.0.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [