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.
Files changed (2) hide show
  1. package/lib/index.js +40 -35
  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;
@@ -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 = "No Introduction";
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
- @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; }
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
- @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; }
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 || "No Data"}</div>
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
- @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; }
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: 20px; flex-shrink: 0;
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.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); }
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>${info.Content}</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: 3 });
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: 90 }));
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 `[Error] ${res.msg}`;
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 `[Error] 读取失败: ${res.msg}`;
440
- await session.send(`[Reading] ${res.data.Title}...`);
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(`[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}`);
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("Navigation:\n" + navs.join("\n"))));
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 "[Error] 渲染失败";
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 "[Error] 获取失败";
466
+ if (!id) return "[错误] 获取失败";
462
467
  const res = await fetchThread(id);
463
- if (!res.valid) return `[Error] ID:${id} 读取失败`;
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 `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
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("[Search] 搜索中...");
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 "Tip: 发送 /ft.read <ID> 阅读";
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("[Success] 订阅成功");
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 ? "[OK] 已退订" : "未找到订阅";
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 = `[Update] ${res.data.Title} 更新了!
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) {
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.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [