koishi-plugin-fimtale-api 0.0.3 → 0.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 +40 -35
- 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;
|
|
@@ -208,7 +213,7 @@ function apply(ctx, config) {
|
|
|
208
213
|
let summary = stripHtml(info.Content);
|
|
209
214
|
if (summary.length < 10 && parent && isChapter) summary = stripHtml(parent.Content);
|
|
210
215
|
if (summary.length > 100) summary = summary.substring(0, 100) + "...";
|
|
211
|
-
if (!summary) summary = "
|
|
216
|
+
if (!summary) summary = "暂无简介";
|
|
212
217
|
const tagsArr = [];
|
|
213
218
|
if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
|
|
214
219
|
if (displayTagsObj?.Rating && displayTagsObj.Rating !== "E") tagsArr.push(displayTagsObj.Rating);
|
|
@@ -221,8 +226,7 @@ function apply(ctx, config) {
|
|
|
221
226
|
<html>
|
|
222
227
|
<head>
|
|
223
228
|
<style>
|
|
224
|
-
|
|
225
|
-
body { margin: 0; padding: 0; font-family: 'Noto Sans SC', sans-serif; background: transparent; }
|
|
229
|
+
body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
|
|
226
230
|
.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; }
|
|
227
231
|
.cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
|
|
228
232
|
.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; }
|
|
@@ -288,8 +292,7 @@ function apply(ctx, config) {
|
|
|
288
292
|
<html>
|
|
289
293
|
<head>
|
|
290
294
|
<style>
|
|
291
|
-
|
|
292
|
-
body { margin: 0; padding: 0; font-family: 'Noto Sans SC', sans-serif; width: 500px; background: transparent; }
|
|
295
|
+
body { margin: 0; padding: 0; font-family: ${fontStack}; width: 500px; background: transparent; }
|
|
293
296
|
.container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 10px; }
|
|
294
297
|
.header { background: #fafafa; padding: 15px 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
|
295
298
|
.header-title { font-size: 16px; font-weight: bold; color: #333; }
|
|
@@ -324,7 +327,7 @@ function apply(ctx, config) {
|
|
|
324
327
|
<div class="top-row"><div class="title">${r.title}</div><div class="id-badge">ID: ${r.id}</div></div>
|
|
325
328
|
<div class="author">By ${r.author} ${r.status ? ` · ${r.status}` : ""}</div>
|
|
326
329
|
<div class="tags">${r.tags.map((t) => `<span class="tag">${t}</span>`).join("")}</div>
|
|
327
|
-
<div class="meta-row">${stats || "
|
|
330
|
+
<div class="meta-row">${stats || "暂无数据"}</div>
|
|
328
331
|
</div></div>`;
|
|
329
332
|
}).join("")}
|
|
330
333
|
</div>
|
|
@@ -340,13 +343,13 @@ function apply(ctx, config) {
|
|
|
340
343
|
return img;
|
|
341
344
|
}, "renderSearchResults");
|
|
342
345
|
const renderReadPages = /* @__PURE__ */ __name(async (info) => {
|
|
346
|
+
const cleanedContent = cleanContent(info.Content);
|
|
343
347
|
const html = `
|
|
344
348
|
<!DOCTYPE html>
|
|
345
349
|
<html>
|
|
346
350
|
<head>
|
|
347
351
|
<style>
|
|
348
|
-
|
|
349
|
-
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: 'Noto Serif SC', serif; }
|
|
352
|
+
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontStack}; }
|
|
350
353
|
#source-container { display: none; }
|
|
351
354
|
.page {
|
|
352
355
|
width: ${config.deviceWidth}px; height: ${config.deviceHeight}px;
|
|
@@ -356,20 +359,22 @@ function apply(ctx, config) {
|
|
|
356
359
|
}
|
|
357
360
|
.page-header {
|
|
358
361
|
font-size: 12px; color: #8d6e63; border-bottom: 2px solid #d7ccc8;
|
|
359
|
-
padding-bottom: 12px; margin-bottom:
|
|
362
|
+
padding-bottom: 12px; margin-bottom: 15px; flex-shrink: 0;
|
|
360
363
|
display: flex; justify-content: space-between; font-weight: bold;
|
|
361
364
|
}
|
|
362
365
|
.page-footer {
|
|
363
366
|
position: absolute; bottom: 15px; left: 0; right: 0; text-align: center;
|
|
364
367
|
font-size: 12px; color: #aaa; font-family: sans-serif;
|
|
365
368
|
}
|
|
366
|
-
.page-content { flex: 1; overflow: hidden; font-size: ${config.fontSize}px; line-height: 1.
|
|
367
|
-
|
|
368
|
-
|
|
369
|
+
.page-content { flex: 1; overflow: hidden; font-size: ${config.fontSize}px; line-height: 1.7; text-align: justify; }
|
|
370
|
+
/* 段落样式优化 */
|
|
371
|
+
p { margin: 0 0 0.6em 0; text-indent: 2em; }
|
|
372
|
+
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); }
|
|
373
|
+
h1, h2, h3 { font-size: 1.1em; margin: 0.5em 0; color: #5d4037; text-indent: 0; font-weight: bold; }
|
|
369
374
|
</style>
|
|
370
375
|
</head>
|
|
371
376
|
<body>
|
|
372
|
-
<div id="source-container"><div class="meta-info" data-title="${info.Title}" data-author="${info.UserName}"></div>${
|
|
377
|
+
<div id="source-container"><div class="meta-info" data-title="${info.Title}" data-author="${info.UserName}"></div>${cleanedContent}</div>
|
|
373
378
|
<div id="output"></div>
|
|
374
379
|
<script>
|
|
375
380
|
function paginate() {
|
|
@@ -414,12 +419,12 @@ function apply(ctx, config) {
|
|
|
414
419
|
try {
|
|
415
420
|
await injectCookies(page);
|
|
416
421
|
await page.setContent(html);
|
|
417
|
-
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor:
|
|
422
|
+
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
|
|
418
423
|
await page.evaluate("paginate()");
|
|
419
424
|
const imgs = [];
|
|
420
425
|
const pages = await page.$$(".page");
|
|
421
426
|
for (const p of pages) {
|
|
422
|
-
imgs.push(await p.screenshot({ type: "jpeg", quality:
|
|
427
|
+
imgs.push(await p.screenshot({ type: "jpeg", quality: 80 }));
|
|
423
428
|
}
|
|
424
429
|
return imgs;
|
|
425
430
|
} finally {
|
|
@@ -429,15 +434,15 @@ function apply(ctx, config) {
|
|
|
429
434
|
ctx.command("ft.info <threadId:string>", "预览作品").action(async ({ session }, threadId) => {
|
|
430
435
|
if (!threadId) return "请输入ID";
|
|
431
436
|
const res = await fetchThread(threadId);
|
|
432
|
-
if (!res.valid) return `[
|
|
437
|
+
if (!res.valid) return `[错误] ${res.msg}`;
|
|
433
438
|
const img = await renderCard(res.data, res.parent);
|
|
434
439
|
return session.send(import_koishi.h.image(img, "image/png"));
|
|
435
440
|
});
|
|
436
441
|
ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
|
|
437
442
|
if (!threadId) return "请输入ID";
|
|
438
443
|
const res = await fetchThread(threadId);
|
|
439
|
-
if (!res.valid) return `[
|
|
440
|
-
await session.send(`[
|
|
444
|
+
if (!res.valid) return `[错误] 读取失败: ${res.msg}`;
|
|
445
|
+
await session.send(`[加载中] ${res.data.Title}...`);
|
|
441
446
|
try {
|
|
442
447
|
const cardImg = await renderCard(res.data, res.parent);
|
|
443
448
|
await session.send(import_koishi.h.image(cardImg, "image/png"));
|
|
@@ -446,33 +451,33 @@ function apply(ctx, config) {
|
|
|
446
451
|
const navs = [];
|
|
447
452
|
if (res.menu?.length) {
|
|
448
453
|
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(`[
|
|
454
|
+
if (idx > 0) navs.push(`[上一章] /ft.read ${res.menu[idx - 1].ID}`);
|
|
455
|
+
if (idx < res.menu.length - 1) navs.push(`[下一章] /ft.read ${res.menu[idx + 1].ID}`);
|
|
451
456
|
}
|
|
452
|
-
if (navs.length) nodes.push((0, import_koishi.h)("message", import_koishi.h.text("
|
|
457
|
+
if (navs.length) nodes.push((0, import_koishi.h)("message", import_koishi.h.text("章节导航:\n" + navs.join("\n"))));
|
|
453
458
|
return session.send((0, import_koishi.h)("message", { forward: true }, nodes));
|
|
454
459
|
} catch (e) {
|
|
455
460
|
ctx.logger("fimtale").error(e);
|
|
456
|
-
return "[
|
|
461
|
+
return "[错误] 渲染失败";
|
|
457
462
|
}
|
|
458
463
|
});
|
|
459
464
|
ctx.command("ft.random", "随机作品").action(async ({ session }) => {
|
|
460
465
|
const id = await fetchRandomId();
|
|
461
|
-
if (!id) return "[
|
|
466
|
+
if (!id) return "[错误] 获取失败";
|
|
462
467
|
const res = await fetchThread(id);
|
|
463
|
-
if (!res.valid) return `[
|
|
468
|
+
if (!res.valid) return `[错误] ID:${id} 读取失败`;
|
|
464
469
|
const img = await renderCard(res.data, res.parent);
|
|
465
470
|
await session.send(import_koishi.h.image(img, "image/png"));
|
|
466
|
-
return
|
|
471
|
+
return `提示: 发送 /ft.read ${res.data.ID} 阅读全文`;
|
|
467
472
|
});
|
|
468
473
|
ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
|
|
469
474
|
if (!keyword) return "请输入关键词";
|
|
470
|
-
await session.send("[
|
|
475
|
+
await session.send("[加载中] 搜索中...");
|
|
471
476
|
const results = await searchThreads(keyword);
|
|
472
477
|
if (!results.length) return "未找到结果。";
|
|
473
478
|
const img = await renderSearchResults(keyword, results);
|
|
474
479
|
await session.send(import_koishi.h.image(img, "image/png"));
|
|
475
|
-
return "
|
|
480
|
+
return "提示: 发送 /ft.read <ID> 阅读";
|
|
476
481
|
});
|
|
477
482
|
ctx.command("ft.sub <threadId:string>", "订阅").action(async ({ session }, threadId) => {
|
|
478
483
|
if (!/^\d+$/.test(threadId)) return "ID错误";
|
|
@@ -481,13 +486,13 @@ function apply(ctx, config) {
|
|
|
481
486
|
const res = await fetchThread(threadId);
|
|
482
487
|
if (!res.valid) return "帖子不存在";
|
|
483
488
|
await ctx.database.create("fimtale_subs", { cid: session.cid, threadId, lastCount: res.data.Comments, lastCheck: Date.now() });
|
|
484
|
-
await session.send("[
|
|
489
|
+
await session.send("[成功] 订阅成功");
|
|
485
490
|
const img = await renderCard(res.data, res.parent);
|
|
486
491
|
return session.send(import_koishi.h.image(img, "image/png"));
|
|
487
492
|
});
|
|
488
493
|
ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
|
|
489
494
|
const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
|
|
490
|
-
return res.matched ? "[
|
|
495
|
+
return res.matched ? "[成功] 已退订" : "未找到订阅";
|
|
491
496
|
});
|
|
492
497
|
ctx.middleware(async (session, next) => {
|
|
493
498
|
if (!config.autoParseLink) return next();
|
|
@@ -510,7 +515,7 @@ function apply(ctx, config) {
|
|
|
510
515
|
if (!res.valid) continue;
|
|
511
516
|
const targets = subs.filter((s) => s.threadId === tid && s.lastCount < res.data.Comments);
|
|
512
517
|
if (targets.length) {
|
|
513
|
-
const msg = `[
|
|
518
|
+
const msg = `[更新] ${res.data.Title} 更新了!
|
|
514
519
|
回复: ${res.data.Comments}
|
|
515
520
|
https://fimtale.com/t/${tid}`;
|
|
516
521
|
for (const sub of targets) {
|