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.
Files changed (2) hide show
  1. package/lib/index.js +52 -42
  2. 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(22).description("正文字号(px)")
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 "Unknown";
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*&nbsp;\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 Error" };
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: "Request Failed" };
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 = "Unknown";
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(/,/g, "") || "0";
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
- const displayCover = isChapter && parent ? parent.Background || extractImage(parent.Content) : info.Background || extractImage(info.Content);
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 > 100) summary = summary.substring(0, 100) + "...";
211
- if (!summary) summary = "No Introduction";
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
- @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
225
- body { margin: 0; padding: 0; font-family: 'Noto Sans SC', sans-serif; background: transparent; }
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: 24px; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; }
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: 8px; font-weight: 400; }
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: 12px 0; max-height: 56px; overflow: hidden; }
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: 15px; }
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: 640, height: 440, deviceScaleFactor: 2 });
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
- @import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
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 || "No Data"}</div>
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
- @import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&display=swap');
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: 20px; flex-shrink: 0;
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.8; text-align: justify; }
367
- p { margin: 0 0 1em 0; text-indent: 2em; }
368
- img { max-width: 100%; height: auto; display: block; margin: 15px auto; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
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>${info.Content}</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: 3 });
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: 90 }));
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 `[Error] ${res.msg}`;
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 `[Error] 读取失败: ${res.msg}`;
440
- await session.send(`[Reading] ${res.data.Title}...`);
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(`[Prev] /ft.read ${res.menu[idx - 1].ID}`);
450
- if (idx < res.menu.length - 1) navs.push(`[Next] /ft.read ${res.menu[idx + 1].ID}`);
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("Navigation:\n" + navs.join("\n"))));
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 "[Error] 渲染失败";
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 "[Error] 获取失败";
471
+ if (!id) return "[错误] 获取失败";
462
472
  const res = await fetchThread(id);
463
- if (!res.valid) return `[Error] ID:${id} 读取失败`;
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("[Search] 搜索中...");
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("[Success] 订阅成功");
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 ? "[OK] 已退订" : "未找到订阅";
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 = `[Update] ${res.data.Title} 更新了!
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) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-fimtale-api",
3
3
  "description": "Koishi插件,从fimtale搜索/订阅/随机获取小说/解析链接等",
4
- "version": "0.0.3",
4
+ "version": "0.0.5",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [