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.
- package/lib/index.js +62 -27
- 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
|
-
|
|
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
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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/
|
|
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
|
|
452
|
-
if (
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
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", {});
|