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.
- package/lib/index.js +62 -36
- 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 =
|
|
197
|
-
if (isChapter && parent)
|
|
198
|
-
|
|
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
|
-
|
|
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>${
|
|
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
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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:
|
|
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:
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
461
|
-
if (
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
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", {});
|