koishi-plugin-fimtale-api 1.0.0 → 1.0.2

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 +156 -234
  2. package/package.json +1 -1
package/lib/index.js CHANGED
@@ -47,7 +47,6 @@ 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
- // 渲染配置 (标准手机比例)
51
50
  deviceWidth: import_koishi.Schema.number().default(390).description("阅读器渲染宽度(px)"),
52
51
  deviceHeight: import_koishi.Schema.number().default(844).description("阅读器渲染高度(px)"),
53
52
  fontSize: import_koishi.Schema.number().default(20).description("正文字号(px)")
@@ -78,7 +77,34 @@ function apply(ctx, config) {
78
77
  }, "generateGradient");
79
78
  const cleanContent = /* @__PURE__ */ __name((html) => {
80
79
  if (!html) return "";
81
- return html.replace(/style\s*=\s*['"][^'"]*['"]/gi, "").replace(/<p[^>]*>\s*(?:<br\s*\/?>|&nbsp;|&#160;|\s| )*\s*<\/p>/gi, "").replace(/(<br\s*\/?>\s*){2,}/gi, "<br>").replace(/<\/p>\s*<br\s*\/?>\s*<p/gi, "</p><p>").replace(/<p[^>]*>\s*<br\s*\/?>/gi, "<p>").replace(/<br\s*\/?>\s*<\/p>/gi, "</p>").replace(/(?:<br\s*\/?>|&nbsp;|\s)+$/gi, "").trim();
80
+ let processed = html;
81
+ processed = processed.replace(/<div class="right">[\s\S]*?<\/div>/i, "");
82
+ processed = processed.replace(/<div class="title-tags"[\s\S]*?<\/div>/i, "");
83
+ processed = processed.replace(/<p class="status-bar[\s\S]*?<\/p>/i, "");
84
+ processed = processed.replace(/<div class="card-panel[\s\S]*?<\/div>/i, "");
85
+ processed = processed.replace(/<script\b[^>]*>([\s\S]*?)<\/script>/gmi, "");
86
+ processed = processed.replace(/<style\b[^>]*>([\s\S]*?)<\/style>/gmi, "");
87
+ processed = processed.replace(/<iframe[^>]*>.*?<\/iframe>/gmi, '<p class="align-center" style="color:#999;font-size:0.8em;">[多媒体内容]</p>');
88
+ processed = processed.replace(
89
+ /<div class="material-placeholder">([\s\S]*?)<\/div>/gi,
90
+ (match, content) => {
91
+ const cleanImg = content.replace(/loading="lazy"/gi, "");
92
+ return `<figure class="img-box">${cleanImg}</figure>`;
93
+ }
94
+ );
95
+ processed = processed.replace(/<p[^>]*style="[^"]*text-align:\s*right[^"]*"[^>]*>/gi, '<p class="align-right">');
96
+ processed = processed.replace(/<p[^>]*style="[^"]*text-align:\s*center[^"]*"[^>]*>/gi, '<p class="align-center">');
97
+ processed = processed.replace(/<p[^>]*style="[^"]*text-indent:\s*0[^"]*"[^>]*>/gi, '<p class="no-indent">');
98
+ processed = processed.replace(/<\/?div[^>]*>/gi, "");
99
+ processed = processed.replace(/<blockquote[^>]*>/gi, "<blockquote>");
100
+ processed = processed.replace(/class=(["'])(?!(align-center|align-right|no-indent|img-box)\1)[^"']*\1/gi, "");
101
+ processed = processed.replace(/style\s*=\s*['"][^'"]*['"]/gi, "");
102
+ processed = processed.replace(/<\/?(span|font|o:p)[^>]*>/gi, "");
103
+ processed = processed.replace(/&nbsp;/gi, " ");
104
+ processed = processed.replace(/<p[^>]*>\s*<\/p>/gi, "").replace(/<\/p>\s*<br\s*\/?>\s*<p/gi, "</p><p>").replace(/<p[^>]*>\s*(?:<br\s*\/?>|\s)+/gi, "<p>").replace(/(?:<br\s*\/?>|\s)+<\/p>/gi, "</p>");
105
+ processed = processed.replace(/ +/g, " ");
106
+ processed = processed.replace(/(?:<br\s*\/?>|\s)+$/gi, "");
107
+ return processed.trim();
82
108
  }, "cleanContent");
83
109
  const fontStack = '"Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif';
84
110
  const fontSerif = '"Noto Serif SC", "Source Han Serif SC", "SimSun", serif';
@@ -97,12 +123,7 @@ function apply(ctx, config) {
97
123
  const params = { APIKey: config.apiKey, APIPass: config.apiPass };
98
124
  const res = await ctx.http.get(url, { params });
99
125
  if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API Error" };
100
- return {
101
- valid: true,
102
- data: res.TopicInfo,
103
- parent: res.ParentInfo,
104
- menu: res.Menu || []
105
- };
126
+ return { valid: true, data: res.TopicInfo, parent: res.ParentInfo, menu: res.Menu || [] };
106
127
  } catch (e) {
107
128
  return { valid: false, msg: "Request Failed" };
108
129
  }
@@ -129,67 +150,40 @@ function apply(ctx, config) {
129
150
  await page.waitForSelector(".card", { timeout: 5e3 });
130
151
  } catch {
131
152
  }
132
- const results = await page.evaluate(() => {
153
+ return await page.evaluate(() => {
133
154
  const items = [];
134
- const cards = document.querySelectorAll(".card.topic-card");
135
- cards.forEach((card) => {
155
+ document.querySelectorAll(".card.topic-card").forEach((card) => {
136
156
  if (items.length >= 6) return;
137
157
  const link = card.querySelector('a[href^="/t/"]');
138
- const href = link?.getAttribute("href");
139
- const idMatch = href?.match(/^\/t\/(\d+)$/);
140
- if (!idMatch) return;
141
- const id = idMatch[1];
142
- if (items.some((i) => i.id === id)) return;
143
- let title = "";
144
- const titleEl = card.querySelector(".card-title");
145
- if (titleEl) title = titleEl.textContent?.trim() || "";
146
- else title = link.textContent?.trim() || "";
147
- let author = "Unknown";
148
- const authorEl = card.querySelector('a[href^="/u/"] span.grey-text');
149
- if (authorEl) author = authorEl.textContent?.trim() || "";
150
- let cover = void 0;
151
- const imgEl = card.querySelector(".card-image img");
152
- if (imgEl) {
153
- const src = imgEl.getAttribute("src");
154
- if (src && (!src.includes("avatar") || src.includes("upload"))) cover = src;
155
- }
158
+ const id = link?.getAttribute("href")?.match(/^\/t\/(\d+)$/)?.[1];
159
+ if (!id || items.some((i) => i.id === id)) return;
160
+ let title = card.querySelector(".card-title")?.textContent?.trim() || link.textContent?.trim() || "";
161
+ let author = card.querySelector('a[href^="/u/"] span.grey-text')?.textContent?.trim() || "";
162
+ let cover = card.querySelector(".card-image img")?.getAttribute("src");
163
+ if (cover && (cover.includes("avatar") && !cover.includes("upload"))) cover = void 0;
156
164
  const tags = [];
157
- let status = "";
158
- card.querySelectorAll(".main-tag-set div").forEach((b) => {
159
- const t = b.textContent?.trim();
160
- if (t) tags.push(t);
161
- });
162
- card.querySelectorAll(".chip").forEach((c) => {
165
+ card.querySelectorAll(".main-tag-set div, .chip").forEach((c) => {
163
166
  const t = c.textContent?.trim();
164
- if (!t) return;
165
- if (["连载中", "已完结", "已弃坑"].includes(t)) status = t;
166
- else if (!t.includes("展开其余")) tags.push(t);
167
+ if (t && !["连载中", "已完结", "已弃坑"].includes(t) && !t.includes("展开")) tags.push(t);
167
168
  });
169
+ const status = Array.from(card.querySelectorAll(".chip")).find((c) => ["连载中", "已完结", "已弃坑"].includes(c.textContent?.trim() || ""))?.textContent?.trim() || "";
168
170
  const stats = { views: "0", comments: "0", likes: "0", words: "0" };
169
- const actionDiv = card.querySelector(".card-action > div");
170
- if (actionDiv) {
171
- actionDiv.querySelectorAll("span[title]").forEach((s) => {
172
- const t = s.getAttribute("title") || "";
173
- const v = s.textContent?.trim().replace(/[^0-9]/g, "") || "0";
174
- if (t.includes("")) stats.words = v;
175
- if (t.includes("阅读")) stats.views = v;
176
- if (t.includes("评论")) stats.comments = v;
177
- });
178
- }
179
- const greenText = card.querySelector(".left.green-text");
180
- if (greenText) stats.likes = greenText.textContent?.replace(/[^0-9]/g, "") || "0";
171
+ card.querySelectorAll(".card-action > div span[title]").forEach((s) => {
172
+ const t = s.getAttribute("title") || "";
173
+ const v = s.textContent?.trim().replace(/[^0-9]/g, "") || "0";
174
+ if (t.includes("")) stats.words = v;
175
+ if (t.includes("阅读")) stats.views = v;
176
+ if (t.includes("评论")) stats.comments = v;
177
+ });
178
+ stats.likes = card.querySelector(".left.green-text")?.textContent?.replace(/[^0-9]/g, "") || "0";
181
179
  let updateTime = "";
182
- const timeSpan = card.querySelector('div[style*="margin: 3px 0;"] span.grey-text');
183
- if (timeSpan) {
184
- const txt = timeSpan.textContent || "";
185
- const dateMatch = txt.match(/(\d{4}\s*年\s*\d{1,2}\s*月\s*\d{1,2}\s*日)/) || txt.match(/(\d+\s*(?:小时|分钟|天)前)/) || txt.match(/(\d{1,2}\s*月\s*\d{1,2}\s*日)/);
186
- if (dateMatch) updateTime = dateMatch[1].replace(/\s/g, "");
187
- }
180
+ const timeTxt = card.querySelector('div[style*="margin: 3px 0;"] span.grey-text')?.textContent || "";
181
+ 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*日)/);
182
+ if (dateMatch) updateTime = dateMatch[1].replace(/\s/g, "");
188
183
  items.push({ id, title, author, cover, tags: tags.slice(0, 8), status, stats, updateTime });
189
184
  });
190
185
  return items;
191
186
  });
192
- return results;
193
187
  } catch (e) {
194
188
  return [];
195
189
  } finally {
@@ -200,18 +194,10 @@ function apply(ctx, config) {
200
194
  const isChapter = info.IsChapter || !!parent && parent.ID !== info.ID;
201
195
  const displayTitle = isChapter && parent ? parent.Title : info.Title;
202
196
  let displayCover = null;
203
- if (isChapter && parent) {
204
- displayCover = parent.Background || extractImage(parent.Content);
205
- }
206
- if (!displayCover) {
207
- displayCover = info.Background || extractImage(info.Content);
208
- }
197
+ if (isChapter && parent) displayCover = parent.Background || extractImage(parent.Content);
198
+ if (!displayCover) displayCover = info.Background || extractImage(info.Content);
209
199
  const displayTagsObj = isChapter && parent ? parent.Tags : info.Tags;
210
200
  const subTitle = isChapter ? info.Title : null;
211
- const views = info.Views || 0;
212
- const comments = info.Comments || 0;
213
- const words = info.WordCount || 0;
214
- const likes = info.Upvotes || 0;
215
201
  const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
216
202
  let summary = stripHtml(info.Content);
217
203
  if (summary.length < 10 && parent && isChapter) summary = stripHtml(parent.Content);
@@ -220,136 +206,73 @@ function apply(ctx, config) {
220
206
  const tagsArr = [];
221
207
  if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
222
208
  if (displayTagsObj?.Rating && displayTagsObj.Rating !== "E") tagsArr.push(displayTagsObj.Rating);
223
- if (displayTagsObj?.OtherTags && Array.isArray(displayTagsObj.OtherTags)) {
224
- tagsArr.push(...displayTagsObj.OtherTags);
225
- }
226
- const displayTags = tagsArr.slice(0, 10);
227
- const html = `
228
- <!DOCTYPE html>
229
- <html>
230
- <head>
231
- <style>
209
+ if (displayTagsObj?.OtherTags) tagsArr.push(...displayTagsObj.OtherTags);
210
+ const html = `<!DOCTYPE html><html><head><style>
232
211
  body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
233
212
  .card { width: 620px; height: 360px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
234
213
  .cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
235
214
  .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; }
236
215
  .info { flex: 1; padding: 24px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
237
-
238
- .header-group { flex-shrink: 0; margin-bottom: 10px; }
239
- .title {
240
- font-size: 22px; font-weight: 700; color: #333; line-height: 1.4;
241
- display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
242
- margin-bottom: 4px;
243
- }
244
- .subtitle {
245
- font-size: 15px; color: #555; font-weight: 500;
246
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
247
- padding-left: 10px; border-left: 3px solid #e91e63;
248
- margin-top: 4px;
249
- }
216
+ .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; }
217
+ .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; }
250
218
  .author { font-size: 13px; color: #888; margin-top: 6px; font-weight: 400; }
251
-
252
219
  .tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; flex-shrink: 0; }
253
220
  .tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
254
- .tag-imp { background: #e3f2fd; color: #1565c0; }
255
-
256
221
  .summary-box { flex: 1; position: relative; overflow: hidden; min-height: 0; }
257
- .summary {
258
- font-size: 13px; color: #666; line-height: 1.6;
259
- display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
260
- padding-bottom: 3px;
261
- }
262
-
263
- .footer {
264
- border-top: 1px solid #eee; padding-top: 14px;
265
- display: flex; justify-content: space-between;
266
- font-size: 12px; color: #888; margin-top: auto; flex-shrink: 0;
267
- }
222
+ .summary { font-size: 13px; color: #666; line-height: 1.6; display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden; padding-bottom: 3px; }
223
+ .footer { border-top: 1px solid #eee; padding-top: 14px; display: flex; justify-content: space-between; font-size: 12px; color: #888; margin-top: auto; flex-shrink: 0; }
268
224
  .stat b { color: #555; font-weight: bold; margin-right: 2px;}
269
- </style>
270
- </head>
271
- <body>
272
- <div class="card">
273
- <div class="cover"><div class="id-tag">ID: ${info.ID}</div></div>
225
+ </style></head><body>
226
+ <div class="card"><div class="cover"><div class="id-tag">ID: ${info.ID}</div></div>
274
227
  <div class="info">
275
- <div class="header-group">
276
- <div class="title">${displayTitle}</div>
277
- ${subTitle ? `<div class="subtitle">${subTitle}</div>` : ""}
278
- <div class="author">@${info.UserName}</div>
279
- </div>
280
- <div class="tags">${displayTags.map((t) => `<span class="tag ${["文", "译", "R"].includes(t) ? "tag-imp" : ""}">${t}</span>`).join("")}</div>
228
+ <div class="header-group"><div class="title">${displayTitle}</div>${subTitle ? `<div class="subtitle">${subTitle}</div>` : ""}<div class="author">@${info.UserName}</div></div>
229
+ <div class="tags">${tagsArr.slice(0, 10).map((t) => `<span class="tag">${t}</span>`).join("")}</div>
281
230
  <div class="summary-box"><div class="summary">${summary}</div></div>
282
231
  <div class="footer">
283
- <span class="stat"><b style="color:#009688">热度</b>${views}</span>
284
- <span class="stat"><b style="color:#673ab7">评论</b>${comments}</span>
285
- <span class="stat"><b style="color:#4caf50">赞</b>${likes}</span>
286
- <span class="stat"><b style="color:#795548">字数</b>${words}</span>
287
- </div>
288
- </div>
289
- </div>
290
- </body></html>`;
232
+ <span class="stat"><b style="color:#009688">热度</b>${info.Views || 0}</span><span class="stat"><b style="color:#673ab7">评论</b>${info.Comments || 0}</span>
233
+ <span class="stat"><b style="color:#4caf50">赞</b>${info.Upvotes || 0}</span><span class="stat"><b style="color:#795548">字数</b>${info.WordCount || 0}</span>
234
+ </div></div></div></body></html>`;
291
235
  const page = await ctx.puppeteer.page();
292
236
  await injectCookies(page);
293
237
  await page.setContent(html);
294
238
  await page.setViewport({ width: 660, height: 480, deviceScaleFactor: 2 });
295
- const el = await page.$(".card");
296
- const img = await el.screenshot({ type: "png" });
239
+ const img = await page.$(".card").then((e) => e.screenshot({ type: "png" }));
297
240
  await page.close();
298
241
  return img;
299
242
  }, "renderCard");
300
243
  const renderSearchResults = /* @__PURE__ */ __name(async (keyword, results) => {
301
- const html = `
302
- <!DOCTYPE html>
303
- <html>
304
- <head>
305
- <style>
244
+ const html = `<!DOCTYPE html><html><head><style>
306
245
  body { margin: 0; padding: 0; font-family: ${fontStack}; width: 500px; background: transparent; }
307
246
  .container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 10px; }
308
247
  .header { background: #fafafa; padding: 15px 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
309
248
  .header-title { font-size: 16px; font-weight: bold; color: #333; }
310
- .list { padding: 0; }
311
249
  .item { display: flex; padding: 15px; border-bottom: 1px solid #f5f5f5; height: 110px; align-items: flex-start; }
312
- .cover-box { width: 75px; height: 100%; border-radius: 6px; overflow: hidden; flex-shrink: 0; margin-right: 15px; background: #eee; position: relative; }
250
+ .cover-box { width: 75px; height: 100%; border-radius: 6px; overflow: hidden; flex-shrink: 0; margin-right: 15px; background: #eee; }
313
251
  .cover-img { width: 100%; height: 100%; object-fit: cover; }
314
252
  .content { flex: 1; display: flex; flex-direction: column; justify-content: space-between; height: 100%; min-width: 0; }
315
253
  .top-row { display: flex; justify-content: space-between; align-items: flex-start; }
316
- .title { font-size: 16px; font-weight: bold; color: #222; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 4px; flex:1; margin-right: 8px;}
254
+ .title { font-size: 16px; font-weight: bold; color: #222; line-height: 1.3; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; flex:1; margin-right: 8px;}
317
255
  .id-badge { background: #455a64; color: #fff; padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 11px; font-weight: bold; flex-shrink: 0; }
318
256
  .author { font-size: 12px; color: #666; }
319
257
  .tags { display: flex; gap: 4px; flex-wrap: wrap; height: 18px; overflow: hidden; margin-top: 4px; }
320
258
  .tag { background: #f3f3f3; color: #666; padding: 0 5px; border-radius: 3px; font-size: 10px; white-space: nowrap; line-height: 1.6;}
321
259
  .meta-row { display: flex; gap: 10px; font-size: 11px; color: #999; margin-top: auto; border-top: 1px dashed #eee; padding-top: 5px; }
322
260
  .stat b { margin-right: 1px; font-weight: bold; }
323
- </style>
324
- </head>
325
- <body>
326
- <div class="container">
327
- <div class="header"><div class="header-title">🔍 "${keyword}"</div><div>Top ${results.length}</div></div>
328
- <div class="list">
261
+ </style></head><body><div class="container"><div class="header"><div class="header-title">🔍 "${keyword}"</div><div>Top ${results.length}</div></div><div class="list">
329
262
  ${results.map((r) => {
330
263
  const bg = r.cover ? `<img class="cover-img" src="${r.cover}"/>` : `<div style="width:100%;height:100%;background:${generateGradient(r.title)}"></div>`;
331
- const stats = [
332
- r.stats.views && r.stats.views != "0" ? `<span class="stat" style="color:#009688"><b>热</b>${r.stats.views}</span>` : "",
333
- r.stats.comments && r.stats.comments != "0" ? `<span class="stat" style="color:#673ab7"><b>评</b>${r.stats.comments}</span>` : "",
334
- r.stats.likes && r.stats.likes != "0" ? `<span class="stat" style="color:#4caf50"><b>赞</b>${r.stats.likes}</span>` : "",
335
- r.updateTime ? `<span class="stat" style="margin-left:auto;color:#757575">${r.updateTime}</span>` : ""
336
- ].join("");
337
264
  return `<div class="item"><div class="cover-box">${bg}</div><div class="content">
338
265
  <div class="top-row"><div class="title">${r.title}</div><div class="id-badge">ID: ${r.id}</div></div>
339
266
  <div class="author">By ${r.author} ${r.status ? ` · ${r.status}` : ""}</div>
340
267
  <div class="tags">${r.tags.map((t) => `<span class="tag">${t}</span>`).join("")}</div>
341
- <div class="meta-row">${stats || "No Data"}</div>
268
+ <div class="meta-row"><span style="color:#009688"><b>热</b>${r.stats.views}</span><span style="color:#673ab7"><b>评</b>${r.stats.comments}</span><span style="color:#4caf50"><b>赞</b>${r.stats.likes}</span><span style="margin-left:auto;color:#757575">${r.updateTime}</span></div>
342
269
  </div></div>`;
343
270
  }).join("")}
344
- </div>
345
- </div>
346
- </body></html>`;
271
+ </div></div></body></html>`;
347
272
  const page = await ctx.puppeteer.page();
348
273
  await page.setContent(html);
349
274
  await page.setViewport({ width: 550, height: 800, deviceScaleFactor: 2 });
350
- await sleep(300);
351
- const el = await page.$(".container");
352
- const img = await el.screenshot({ type: "png" });
275
+ const img = await page.$(".container").then((e) => e.screenshot({ type: "png" }));
353
276
  await page.close();
354
277
  return img;
355
278
  }, "renderSearchResults");
@@ -359,7 +282,7 @@ function apply(ctx, config) {
359
282
  const footerHeight = 30;
360
283
  const paddingX = 25;
361
284
  const paddingY = 20;
362
- const lineHeightRatio = 1.8;
285
+ const lineHeightRatio = 1.6;
363
286
  const contentWidth = config.deviceWidth - paddingX * 2;
364
287
  const columnGap = 40;
365
288
  const maxContentHeight = config.deviceHeight - headerHeight - footerHeight;
@@ -367,88 +290,93 @@ function apply(ctx, config) {
367
290
  const linesPerPage = Math.floor((maxContentHeight - paddingY * 2) / lineHeightPx);
368
291
  const optimalContentHeight = linesPerPage * lineHeightPx + paddingY * 2;
369
292
  const marginTop = Math.floor((maxContentHeight - optimalContentHeight) / 2) + headerHeight;
370
- const html = `
371
- <!DOCTYPE html>
372
- <html>
373
- <head>
374
- <style>
293
+ const html = `<!DOCTYPE html><html><head><style>
375
294
  body { margin: 0; padding: 0; width: ${config.deviceWidth}px; height: ${config.deviceHeight}px; background-color: #f6f4ec; color: #2c2c2c; font-family: ${fontSerif}; overflow: hidden; position: relative;}
295
+ .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; }
296
+ .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; }
297
+ #viewport { position: absolute; top: ${marginTop}px; left: ${paddingX}px; width: ${contentWidth}px; height: ${optimalContentHeight}px; overflow: hidden; }
376
298
 
377
- .fixed-header {
378
- position: absolute; top: 0; left: 0; width: 100%; height: ${headerHeight}px;
379
- border-bottom: 1px solid #d7ccc8; box-sizing: border-box;
380
- padding: 0 20px; display: flex; align-items: center; justify-content: space-between;
381
- font-size: 12px; color: #8d6e63; background: #f6f4ec; z-index: 5; font-weight: bold;
299
+ #content-scroller {
300
+ height: 100%; width: 100%;
301
+ column-width: ${contentWidth}px; column-gap: ${columnGap}px; column-fill: auto;
302
+ padding: ${paddingY}px 0; box-sizing: border-box;
303
+ font-size: ${config.fontSize}px; line-height: ${lineHeightRatio};
304
+ text-align: left; /* 修正:左对齐防止空格拉伸 */
305
+ transform: translateX(0); transition: none;
382
306
  }
383
307
 
384
- .fixed-footer {
385
- position: absolute; bottom: 0; left: 0; width: 100%; height: ${footerHeight}px;
386
- display: flex; align-items: center; justify-content: center;
387
- font-size: 12px; color: #aaa; background: #f6f4ec; z-index: 5;
388
- }
389
-
390
- /* 视口容器:限定只显示一页内容 */
391
- #viewport {
392
- position: absolute;
393
- top: ${marginTop}px;
394
- left: ${paddingX}px;
395
- width: ${contentWidth}px;
396
- height: ${optimalContentHeight}px;
397
- overflow: hidden;
398
- }
399
-
400
- /* 长条容器:包含所有列 */
401
- #content-scroller {
402
- height: 100%;
403
- width: 100%;
404
-
405
- /* CSS Columns 布局 */
406
- column-width: ${contentWidth}px;
407
- column-gap: ${columnGap}px;
408
- column-fill: auto;
409
-
410
- padding: ${paddingY}px 0;
411
- box-sizing: border-box;
412
-
413
- font-size: ${config.fontSize}px;
414
- line-height: ${lineHeightRatio};
415
- text-align: justify;
416
-
417
- transform: translateX(0);
418
- transition: none;
419
- }
308
+ /* 基础文本 */
309
+ p, div { margin: 0 0 0.2em 0; text-indent: 2em; word-wrap: break-word; overflow-wrap: break-word; }
420
310
 
421
- /* 段落间距优化:0.4em 是一个平衡点,紧凑但区分清晰 */
422
- p {
423
- margin: 0 0 0.4em 0;
424
- text-indent: 2em;
425
- }
426
- /* 防止最后一段下边距导致额外分页 */
427
- p:last-child { margin-bottom: 0; }
311
+ /* 辅助类 */
312
+ .align-center { text-align: center !important; text-align-last: center !important; text-indent: 0 !important; margin: 0.8em 0; font-weight: bold; color: #5d4037; }
313
+ .align-right { text-align: right !important; text-indent: 0 !important; margin-top: 0.5em; color: #666; font-style: italic; }
314
+ .no-indent { text-indent: 0 !important; }
315
+
316
+ /* 富文本支持 */
317
+ blockquote { margin: 1em 0.5em; padding-left: 1em; border-left: 4px solid #d7ccc8; color: #666; }
318
+ blockquote p { text-indent: 0; margin: 0.3em 0; }
319
+
320
+ ul, ol { margin: 0.5em 0; padding-left: 1.5em; }
321
+ li { margin-bottom: 0.2em; }
322
+
323
+ hr { border: 0; height: 1px; background: #d7ccc8; margin: 1.5em 0; }
324
+
325
+ table { width: 100%; border-collapse: collapse; margin: 1em 0; font-size: 0.9em; }
326
+ th, td { border: 1px solid #ccc; padding: 4px; text-align: left; }
327
+ th { background: #eee; font-weight: bold; }
428
328
 
429
- img { max-width: 100%; height: auto; display: block; margin: 10px auto; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); }
430
- h1, h2, h3 { font-size: 1.1em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; break-after: avoid; break-inside: avoid; }
431
- </style>
432
- </head>
433
- <body>
434
- <div class="fixed-header">
435
- <span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span>
436
- <span>${info.UserName}</span>
437
- </div>
438
-
439
- <div id="viewport">
440
- <div id="content-scroller">
441
- ${content}
442
- </div>
443
- </div>
329
+ pre { background: #eee; padding: 0.5em; overflow-x: auto; border-radius: 4px; margin: 0.5em 0; }
330
+ code { font-family: monospace; background: #f0f0f0; padding: 2px 4px; border-radius: 3px; }
331
+
332
+ s, strike, del { text-decoration: line-through; color: #888; }
333
+ u { text-decoration: underline; }
334
+ sup, sub { font-size: 0.75em; line-height: 0; position: relative; vertical-align: baseline; }
335
+ sup { top: -0.5em; }
336
+ sub { bottom: -0.25em; }
337
+
338
+ a { color: #0277bd; text-decoration: none; }
444
339
 
445
- <div class="fixed-footer" id="page-indicator">- 1 -</div>
446
- </body></html>`;
340
+ /* 图片 (使用 figure 替代 div) */
341
+ figure.img-box { display: flex; justify-content: center; align-items: center; margin: 0.5em 0; width: 100%; }
342
+ img { max-width: 100%; height: auto; display: block; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); }
343
+
344
+ /* 标题 */
345
+ h1, h2, h3 { font-size: 1.1em; margin: 0.8em 0; color: #5d4037; text-indent: 0; font-weight: bold; text-align: center; text-align-last: center; break-after: avoid; }
346
+
347
+ strong, b { font-weight: 900; color: #3e2723; }
348
+ em, i { font-style: italic; }
349
+
350
+ p:last-child { margin-bottom: 0; }
351
+ </style></head><body>
352
+ <div class="fixed-header"><span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span><span>${info.UserName}</span></div>
353
+ <div id="viewport"><div id="content-scroller">${content}</div></div>
354
+ <div class="fixed-footer" id="page-indicator">- 1 -</div></body></html>`;
447
355
  const page = await ctx.puppeteer.page();
448
356
  try {
449
357
  await injectCookies(page);
450
358
  await page.setContent(html);
451
359
  await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 2 });
360
+ await page.evaluate(async () => {
361
+ await document.fonts.ready;
362
+ await new Promise((resolve) => {
363
+ const imgs2 = Array.from(document.images);
364
+ if (!imgs2.length) return resolve(true);
365
+ let loaded = 0;
366
+ imgs2.forEach((img) => {
367
+ if (img.complete) {
368
+ loaded++;
369
+ if (loaded === imgs2.length) resolve(true);
370
+ } else {
371
+ img.onload = img.onerror = () => {
372
+ loaded++;
373
+ if (loaded === imgs2.length) resolve(true);
374
+ };
375
+ }
376
+ });
377
+ setTimeout(resolve, 3e3);
378
+ });
379
+ });
452
380
  const scrollWidth = await page.$eval("#content-scroller", (el) => el.scrollWidth);
453
381
  const step = contentWidth + columnGap;
454
382
  const totalPages = Math.floor((scrollWidth + columnGap - 10) / step) + 1;
@@ -460,8 +388,7 @@ function apply(ctx, config) {
460
388
  document.getElementById("content-scroller").style.transform = `translateX(${offset}px)`;
461
389
  document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
462
390
  }, i, step, i + 1, finalPages);
463
- const img = await page.screenshot({ type: "jpeg", quality: 80 });
464
- imgs.push(img);
391
+ imgs.push(await page.screenshot({ type: "jpeg", quality: 80 }));
465
392
  }
466
393
  return imgs;
467
394
  } finally {
@@ -472,8 +399,7 @@ function apply(ctx, config) {
472
399
  if (!threadId) return "请输入ID";
473
400
  const res = await fetchThread(threadId);
474
401
  if (!res.valid) return `[错误] ${res.msg}`;
475
- const img = await renderCard(res.data, res.parent);
476
- return session.send(import_koishi.h.image(img, "image/png"));
402
+ return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
477
403
  });
478
404
  ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
479
405
  if (!threadId) return "请输入ID";
@@ -503,8 +429,7 @@ function apply(ctx, config) {
503
429
  if (!id) return "[错误] 获取失败";
504
430
  const res = await fetchThread(id);
505
431
  if (!res.valid) return `[错误] ID:${id} 读取失败`;
506
- const img = await renderCard(res.data, res.parent);
507
- await session.send(import_koishi.h.image(img, "image/png"));
432
+ await session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
508
433
  return `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
509
434
  });
510
435
  ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
@@ -512,8 +437,7 @@ function apply(ctx, config) {
512
437
  await session.send("[加载中] 搜索中...");
513
438
  const results = await searchThreads(keyword);
514
439
  if (!results.length) return "未找到结果。";
515
- const img = await renderSearchResults(keyword, results);
516
- await session.send(import_koishi.h.image(img, "image/png"));
440
+ await session.send(import_koishi.h.image(await renderSearchResults(keyword, results), "image/png"));
517
441
  const exampleId = results[0]?.id || "12345";
518
442
  return `Tip: 发送 /ft.read [ID] 阅读 (例: /ft.read ${exampleId})`;
519
443
  });
@@ -525,8 +449,7 @@ function apply(ctx, config) {
525
449
  if (!res.valid) return "帖子不存在";
526
450
  await ctx.database.create("fimtale_subs", { cid: session.cid, threadId, lastCount: res.data.Comments, lastCheck: Date.now() });
527
451
  await session.send("[成功] 订阅成功");
528
- const img = await renderCard(res.data, res.parent);
529
- return session.send(import_koishi.h.image(img, "image/png"));
452
+ return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
530
453
  });
531
454
  ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
532
455
  const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
@@ -538,8 +461,7 @@ function apply(ctx, config) {
538
461
  if (match && match[1] && session.userId !== session.selfId) {
539
462
  const res = await fetchThread(match[1]);
540
463
  if (res.valid) {
541
- const img = await renderCard(res.data, res.parent);
542
- session.send(import_koishi.h.image(img, "image/png"));
464
+ session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
543
465
  }
544
466
  }
545
467
  return next();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-fimtale-api",
3
3
  "description": "Koishi插件,从fimtale搜索/订阅/随机获取小说/解析链接等",
4
- "version": "1.0.0",
4
+ "version": "1.0.2",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [