koishi-plugin-fimtale-api 0.0.3 → 0.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.
- package/lib/index.js +52 -42
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -49,7 +49,7 @@ var Config = import_koishi.Schema.object({
|
|
|
49
49
|
autoParseLink: import_koishi.Schema.boolean().default(true).description("自动解析链接为预览卡片"),
|
|
50
50
|
deviceWidth: import_koishi.Schema.number().default(390).description("阅读器渲染宽度(px)"),
|
|
51
51
|
deviceHeight: import_koishi.Schema.number().default(844).description("阅读器渲染高度(px)"),
|
|
52
|
-
fontSize: import_koishi.Schema.number().default(
|
|
52
|
+
fontSize: import_koishi.Schema.number().default(20).description("正文字号(px)")
|
|
53
53
|
});
|
|
54
54
|
function apply(ctx, config) {
|
|
55
55
|
ctx.model.extend("fimtale_subs", {
|
|
@@ -61,7 +61,7 @@ function apply(ctx, config) {
|
|
|
61
61
|
}, { primary: "id", autoInc: true });
|
|
62
62
|
const sleep = /* @__PURE__ */ __name((ms) => new Promise((resolve) => setTimeout(resolve, ms)), "sleep");
|
|
63
63
|
const formatDate = /* @__PURE__ */ __name((timestamp) => {
|
|
64
|
-
if (!timestamp) return "
|
|
64
|
+
if (!timestamp) return "未知日期";
|
|
65
65
|
const date = new Date(timestamp * 1e3);
|
|
66
66
|
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
|
67
67
|
}, "formatDate");
|
|
@@ -80,6 +80,11 @@ function apply(ctx, config) {
|
|
|
80
80
|
const c2 = "#" + hash.substring(6, 12);
|
|
81
81
|
return `linear-gradient(135deg, ${c1} 0%, ${c2} 100%)`;
|
|
82
82
|
}, "generateGradient");
|
|
83
|
+
const cleanContent = /* @__PURE__ */ __name((html) => {
|
|
84
|
+
if (!html) return "";
|
|
85
|
+
return html.replace(/<p>\s* \s*<\/p>/gi, "").replace(/<p>\s*<br\s*\/?>\s*<\/p>/gi, "").replace(/<p>\s*<\/p>/gi, "").replace(/(<br\s*\/?>){2,}/gi, "<br>").replace(/margin-bottom:\s*\d+px/gi, "");
|
|
86
|
+
}, "cleanContent");
|
|
87
|
+
const fontStack = '"Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "Noto Sans SC", "WenQuanYi Micro Hei", Arial, sans-serif';
|
|
83
88
|
const injectCookies = /* @__PURE__ */ __name(async (page) => {
|
|
84
89
|
if (!config.cookies) return;
|
|
85
90
|
const cookies = config.cookies.split(";").map((pair) => {
|
|
@@ -94,7 +99,7 @@ function apply(ctx, config) {
|
|
|
94
99
|
const url = `${config.apiUrl}/t/${threadId}`;
|
|
95
100
|
const params = { APIKey: config.apiKey, APIPass: config.apiPass };
|
|
96
101
|
const res = await ctx.http.get(url, { params });
|
|
97
|
-
if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API
|
|
102
|
+
if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API 返回错误" };
|
|
98
103
|
return {
|
|
99
104
|
valid: true,
|
|
100
105
|
data: res.TopicInfo,
|
|
@@ -102,7 +107,7 @@ function apply(ctx, config) {
|
|
|
102
107
|
menu: res.Menu || []
|
|
103
108
|
};
|
|
104
109
|
} catch (e) {
|
|
105
|
-
return { valid: false, msg: "
|
|
110
|
+
return { valid: false, msg: "网络请求失败" };
|
|
106
111
|
}
|
|
107
112
|
}, "fetchThread");
|
|
108
113
|
const fetchRandomId = /* @__PURE__ */ __name(async () => {
|
|
@@ -142,7 +147,7 @@ function apply(ctx, config) {
|
|
|
142
147
|
const titleEl = card.querySelector(".card-title");
|
|
143
148
|
if (titleEl) title = titleEl.textContent?.trim() || "";
|
|
144
149
|
else title = link.textContent?.trim() || "";
|
|
145
|
-
let author = "
|
|
150
|
+
let author = "未知";
|
|
146
151
|
const authorEl = card.querySelector('a[href^="/u/"] span.grey-text');
|
|
147
152
|
if (authorEl) author = authorEl.textContent?.trim() || "";
|
|
148
153
|
let cover = void 0;
|
|
@@ -168,7 +173,7 @@ function apply(ctx, config) {
|
|
|
168
173
|
if (actionDiv) {
|
|
169
174
|
actionDiv.querySelectorAll("span[title]").forEach((s) => {
|
|
170
175
|
const t = s.getAttribute("title") || "";
|
|
171
|
-
const v = s.textContent?.trim().replace(
|
|
176
|
+
const v = s.textContent?.trim().replace(/[^0-9]/g, "") || "0";
|
|
172
177
|
if (t.includes("字")) stats.words = v;
|
|
173
178
|
if (t.includes("阅读")) stats.views = v;
|
|
174
179
|
if (t.includes("评论")) stats.comments = v;
|
|
@@ -197,7 +202,13 @@ function apply(ctx, config) {
|
|
|
197
202
|
const renderCard = /* @__PURE__ */ __name(async (info, parent) => {
|
|
198
203
|
const isChapter = info.IsChapter || !!parent && parent.ID !== info.ID;
|
|
199
204
|
const displayTitle = isChapter && parent ? parent.Title : info.Title;
|
|
200
|
-
|
|
205
|
+
let displayCover = null;
|
|
206
|
+
if (isChapter) {
|
|
207
|
+
displayCover = extractImage(info.Content);
|
|
208
|
+
}
|
|
209
|
+
if (!displayCover) {
|
|
210
|
+
displayCover = isChapter && parent ? parent.Background || extractImage(parent.Content) : info.Background || extractImage(info.Content);
|
|
211
|
+
}
|
|
201
212
|
const displayTagsObj = isChapter && parent ? parent.Tags : info.Tags;
|
|
202
213
|
const subTitle = isChapter ? info.Title : null;
|
|
203
214
|
const views = info.Views || 0;
|
|
@@ -207,8 +218,8 @@ function apply(ctx, config) {
|
|
|
207
218
|
const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
|
|
208
219
|
let summary = stripHtml(info.Content);
|
|
209
220
|
if (summary.length < 10 && parent && isChapter) summary = stripHtml(parent.Content);
|
|
210
|
-
if (summary.length >
|
|
211
|
-
if (!summary) summary = "
|
|
221
|
+
if (summary.length > 110) summary = summary.substring(0, 110) + "...";
|
|
222
|
+
if (!summary) summary = "暂无简介";
|
|
212
223
|
const tagsArr = [];
|
|
213
224
|
if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
|
|
214
225
|
if (displayTagsObj?.Rating && displayTagsObj.Rating !== "E") tagsArr.push(displayTagsObj.Rating);
|
|
@@ -221,12 +232,11 @@ function apply(ctx, config) {
|
|
|
221
232
|
<html>
|
|
222
233
|
<head>
|
|
223
234
|
<style>
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
.card { width: 600px; height: 320px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
|
|
235
|
+
body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
|
|
236
|
+
.card { width: 620px; height: 340px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
|
|
227
237
|
.cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
|
|
228
238
|
.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; }
|
|
229
|
-
.info { flex: 1; padding:
|
|
239
|
+
.info { flex: 1; padding: 22px 26px; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; }
|
|
230
240
|
|
|
231
241
|
.title {
|
|
232
242
|
font-size: 22px; font-weight: 700; color: #333; line-height: 1.35;
|
|
@@ -238,9 +248,9 @@ function apply(ctx, config) {
|
|
|
238
248
|
padding-left: 10px; border-left: 3px solid #e91e63;
|
|
239
249
|
}
|
|
240
250
|
|
|
241
|
-
.author { font-size: 13px; color: #888; margin-top:
|
|
251
|
+
.author { font-size: 13px; color: #888; margin-top: 6px; font-weight: 400; }
|
|
242
252
|
|
|
243
|
-
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin:
|
|
253
|
+
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 10px 0; max-height: 56px; overflow: hidden; align-content: flex-start; }
|
|
244
254
|
.tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
|
|
245
255
|
.tag-imp { background: #e3f2fd; color: #1565c0; }
|
|
246
256
|
|
|
@@ -249,7 +259,7 @@ function apply(ctx, config) {
|
|
|
249
259
|
display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
|
|
250
260
|
margin-top: auto;
|
|
251
261
|
}
|
|
252
|
-
.footer { border-top: 1px solid #eee; padding-top: 14px; display: flex; justify-content: space-between; font-size: 12px; color: #888; margin-top:
|
|
262
|
+
.footer { border-top: 1px solid #eee; padding-top: 14px; display: flex; justify-content: space-between; font-size: 12px; color: #888; margin-top: 12px; }
|
|
253
263
|
.stat b { color: #555; font-weight: bold; margin-right: 2px;}
|
|
254
264
|
</style>
|
|
255
265
|
</head>
|
|
@@ -276,7 +286,7 @@ function apply(ctx, config) {
|
|
|
276
286
|
const page = await ctx.puppeteer.page();
|
|
277
287
|
await injectCookies(page);
|
|
278
288
|
await page.setContent(html);
|
|
279
|
-
await page.setViewport({ width:
|
|
289
|
+
await page.setViewport({ width: 660, height: 460, deviceScaleFactor: 2 });
|
|
280
290
|
const el = await page.$(".card");
|
|
281
291
|
const img = await el.screenshot({ type: "png" });
|
|
282
292
|
await page.close();
|
|
@@ -288,8 +298,7 @@ function apply(ctx, config) {
|
|
|
288
298
|
<html>
|
|
289
299
|
<head>
|
|
290
300
|
<style>
|
|
291
|
-
|
|
292
|
-
body { margin: 0; padding: 0; font-family: 'Noto Sans SC', sans-serif; width: 500px; background: transparent; }
|
|
301
|
+
body { margin: 0; padding: 0; font-family: ${fontStack}; width: 500px; background: transparent; }
|
|
293
302
|
.container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 10px; }
|
|
294
303
|
.header { background: #fafafa; padding: 15px 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
|
295
304
|
.header-title { font-size: 16px; font-weight: bold; color: #333; }
|
|
@@ -324,7 +333,7 @@ function apply(ctx, config) {
|
|
|
324
333
|
<div class="top-row"><div class="title">${r.title}</div><div class="id-badge">ID: ${r.id}</div></div>
|
|
325
334
|
<div class="author">By ${r.author} ${r.status ? ` · ${r.status}` : ""}</div>
|
|
326
335
|
<div class="tags">${r.tags.map((t) => `<span class="tag">${t}</span>`).join("")}</div>
|
|
327
|
-
<div class="meta-row">${stats || "
|
|
336
|
+
<div class="meta-row">${stats || "暂无数据"}</div>
|
|
328
337
|
</div></div>`;
|
|
329
338
|
}).join("")}
|
|
330
339
|
</div>
|
|
@@ -340,13 +349,13 @@ function apply(ctx, config) {
|
|
|
340
349
|
return img;
|
|
341
350
|
}, "renderSearchResults");
|
|
342
351
|
const renderReadPages = /* @__PURE__ */ __name(async (info) => {
|
|
352
|
+
const content = cleanContent(info.Content);
|
|
343
353
|
const html = `
|
|
344
354
|
<!DOCTYPE html>
|
|
345
355
|
<html>
|
|
346
356
|
<head>
|
|
347
357
|
<style>
|
|
348
|
-
|
|
349
|
-
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: 'Noto Serif SC', serif; }
|
|
358
|
+
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontStack}; }
|
|
350
359
|
#source-container { display: none; }
|
|
351
360
|
.page {
|
|
352
361
|
width: ${config.deviceWidth}px; height: ${config.deviceHeight}px;
|
|
@@ -356,20 +365,21 @@ function apply(ctx, config) {
|
|
|
356
365
|
}
|
|
357
366
|
.page-header {
|
|
358
367
|
font-size: 12px; color: #8d6e63; border-bottom: 2px solid #d7ccc8;
|
|
359
|
-
padding-bottom: 12px; margin-bottom:
|
|
368
|
+
padding-bottom: 12px; margin-bottom: 15px; flex-shrink: 0;
|
|
360
369
|
display: flex; justify-content: space-between; font-weight: bold;
|
|
361
370
|
}
|
|
362
371
|
.page-footer {
|
|
363
372
|
position: absolute; bottom: 15px; left: 0; right: 0; text-align: center;
|
|
364
373
|
font-size: 12px; color: #aaa; font-family: sans-serif;
|
|
365
374
|
}
|
|
366
|
-
.page-content { flex: 1; overflow: hidden; font-size: ${config.fontSize}px; line-height: 1.
|
|
367
|
-
p { margin: 0 0
|
|
368
|
-
img { max-width: 100%; height: auto; display: block; margin:
|
|
375
|
+
.page-content { flex: 1; overflow: hidden; font-size: ${config.fontSize}px; line-height: 1.7; text-align: justify; }
|
|
376
|
+
p { margin: 0 0 0.6em 0; text-indent: 2em; }
|
|
377
|
+
img { max-width: 100%; height: auto; display: block; margin: 10px auto; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
|
|
378
|
+
h1, h2, h3 { font-size: 1.1em; margin: 0.5em 0; color: #5d4037; text-indent: 0; font-weight: bold; }
|
|
369
379
|
</style>
|
|
370
380
|
</head>
|
|
371
381
|
<body>
|
|
372
|
-
<div id="source-container"><div class="meta-info" data-title="${info.Title}" data-author="${info.UserName}"></div>${
|
|
382
|
+
<div id="source-container"><div class="meta-info" data-title="${info.Title}" data-author="${info.UserName}"></div>${content}</div>
|
|
373
383
|
<div id="output"></div>
|
|
374
384
|
<script>
|
|
375
385
|
function paginate() {
|
|
@@ -414,12 +424,12 @@ function apply(ctx, config) {
|
|
|
414
424
|
try {
|
|
415
425
|
await injectCookies(page);
|
|
416
426
|
await page.setContent(html);
|
|
417
|
-
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor:
|
|
427
|
+
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
|
|
418
428
|
await page.evaluate("paginate()");
|
|
419
429
|
const imgs = [];
|
|
420
430
|
const pages = await page.$$(".page");
|
|
421
431
|
for (const p of pages) {
|
|
422
|
-
imgs.push(await p.screenshot({ type: "jpeg", quality:
|
|
432
|
+
imgs.push(await p.screenshot({ type: "jpeg", quality: 80 }));
|
|
423
433
|
}
|
|
424
434
|
return imgs;
|
|
425
435
|
} finally {
|
|
@@ -429,15 +439,15 @@ function apply(ctx, config) {
|
|
|
429
439
|
ctx.command("ft.info <threadId:string>", "预览作品").action(async ({ session }, threadId) => {
|
|
430
440
|
if (!threadId) return "请输入ID";
|
|
431
441
|
const res = await fetchThread(threadId);
|
|
432
|
-
if (!res.valid) return `[
|
|
442
|
+
if (!res.valid) return `[错误] ${res.msg}`;
|
|
433
443
|
const img = await renderCard(res.data, res.parent);
|
|
434
444
|
return session.send(import_koishi.h.image(img, "image/png"));
|
|
435
445
|
});
|
|
436
446
|
ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
|
|
437
447
|
if (!threadId) return "请输入ID";
|
|
438
448
|
const res = await fetchThread(threadId);
|
|
439
|
-
if (!res.valid) return `[
|
|
440
|
-
await session.send(`[
|
|
449
|
+
if (!res.valid) return `[错误] 读取失败: ${res.msg}`;
|
|
450
|
+
await session.send(`[加载中] ${res.data.Title}...`);
|
|
441
451
|
try {
|
|
442
452
|
const cardImg = await renderCard(res.data, res.parent);
|
|
443
453
|
await session.send(import_koishi.h.image(cardImg, "image/png"));
|
|
@@ -446,28 +456,28 @@ function apply(ctx, config) {
|
|
|
446
456
|
const navs = [];
|
|
447
457
|
if (res.menu?.length) {
|
|
448
458
|
const idx = res.menu.findIndex((m) => m.ID.toString() === threadId);
|
|
449
|
-
if (idx > 0) navs.push(`[
|
|
450
|
-
if (idx < res.menu.length - 1) navs.push(`[
|
|
459
|
+
if (idx > 0) navs.push(`[上一章] /ft.read ${res.menu[idx - 1].ID}`);
|
|
460
|
+
if (idx < res.menu.length - 1) navs.push(`[下一章] /ft.read ${res.menu[idx + 1].ID}`);
|
|
451
461
|
}
|
|
452
|
-
if (navs.length) nodes.push((0, import_koishi.h)("message", import_koishi.h.text("
|
|
462
|
+
if (navs.length) nodes.push((0, import_koishi.h)("message", import_koishi.h.text("章节导航:\n" + navs.join("\n"))));
|
|
453
463
|
return session.send((0, import_koishi.h)("message", { forward: true }, nodes));
|
|
454
464
|
} catch (e) {
|
|
455
465
|
ctx.logger("fimtale").error(e);
|
|
456
|
-
return "[
|
|
466
|
+
return "[错误] 渲染失败";
|
|
457
467
|
}
|
|
458
468
|
});
|
|
459
469
|
ctx.command("ft.random", "随机作品").action(async ({ session }) => {
|
|
460
470
|
const id = await fetchRandomId();
|
|
461
|
-
if (!id) return "[
|
|
471
|
+
if (!id) return "[错误] 获取失败";
|
|
462
472
|
const res = await fetchThread(id);
|
|
463
|
-
if (!res.valid) return `[
|
|
473
|
+
if (!res.valid) return `[错误] ID:${id} 读取失败`;
|
|
464
474
|
const img = await renderCard(res.data, res.parent);
|
|
465
475
|
await session.send(import_koishi.h.image(img, "image/png"));
|
|
466
476
|
return `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
|
|
467
477
|
});
|
|
468
478
|
ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
|
|
469
479
|
if (!keyword) return "请输入关键词";
|
|
470
|
-
await session.send("[
|
|
480
|
+
await session.send("[加载中] 搜索中...");
|
|
471
481
|
const results = await searchThreads(keyword);
|
|
472
482
|
if (!results.length) return "未找到结果。";
|
|
473
483
|
const img = await renderSearchResults(keyword, results);
|
|
@@ -481,13 +491,13 @@ function apply(ctx, config) {
|
|
|
481
491
|
const res = await fetchThread(threadId);
|
|
482
492
|
if (!res.valid) return "帖子不存在";
|
|
483
493
|
await ctx.database.create("fimtale_subs", { cid: session.cid, threadId, lastCount: res.data.Comments, lastCheck: Date.now() });
|
|
484
|
-
await session.send("[
|
|
494
|
+
await session.send("[成功] 订阅成功");
|
|
485
495
|
const img = await renderCard(res.data, res.parent);
|
|
486
496
|
return session.send(import_koishi.h.image(img, "image/png"));
|
|
487
497
|
});
|
|
488
498
|
ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
|
|
489
499
|
const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
|
|
490
|
-
return res.matched ? "[
|
|
500
|
+
return res.matched ? "[成功] 已退订" : "未找到订阅";
|
|
491
501
|
});
|
|
492
502
|
ctx.middleware(async (session, next) => {
|
|
493
503
|
if (!config.autoParseLink) return next();
|
|
@@ -510,7 +520,7 @@ function apply(ctx, config) {
|
|
|
510
520
|
if (!res.valid) continue;
|
|
511
521
|
const targets = subs.filter((s) => s.threadId === tid && s.lastCount < res.data.Comments);
|
|
512
522
|
if (targets.length) {
|
|
513
|
-
const msg = `[
|
|
523
|
+
const msg = `[更新] ${res.data.Title} 更新了!
|
|
514
524
|
回复: ${res.data.Comments}
|
|
515
525
|
https://fimtale.com/t/${tid}`;
|
|
516
526
|
for (const sub of targets) {
|