koishi-plugin-fimtale-api 1.0.0 → 1.0.1

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 +126 -235
  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,31 @@ 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(/<p[^>]*style="[^"]*text-align:\s*right[^"]*"[^>]*>/gi, '<p class="align-right">');
89
+ processed = processed.replace(/<p[^>]*style="[^"]*text-align:\s*center[^"]*"[^>]*>/gi, '<p class="align-center">');
90
+ processed = processed.replace(/<p[^>]*style="[^"]*text-indent:\s*0[^"]*"[^>]*>/gi, '<p class="no-indent">');
91
+ processed = processed.replace(/<div class="material-placeholder">/gi, '<div class="img-box">');
92
+ processed = processed.replace(/<blockquote[^>]*>/gi, "<blockquote>");
93
+ processed = processed.replace(/<div(?![^>]*class=["']img-box["'])[^>]*>/gi, "<p>");
94
+ processed = processed.replace(/<\/div>/gi, (match, offset, string) => {
95
+ return "</div>";
96
+ });
97
+ processed = processed.replace(/class=(["'])(?!(align-center|align-right|no-indent|img-box)\1)[^"']*\1/gi, "");
98
+ processed = processed.replace(/style\s*=\s*['"][^'"]*['"]/gi, "");
99
+ processed = processed.replace(/<\/?(span|font|o:p)[^>]*>/gi, "");
100
+ processed = processed.replace(/&nbsp;/gi, " ");
101
+ 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>");
102
+ processed = processed.replace(/ +/g, " ");
103
+ processed = processed.replace(/(?:<br\s*\/?>|\s)+$/gi, "");
104
+ return processed.trim();
82
105
  }, "cleanContent");
83
106
  const fontStack = '"Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif';
84
107
  const fontSerif = '"Noto Serif SC", "Source Han Serif SC", "SimSun", serif';
@@ -97,12 +120,7 @@ function apply(ctx, config) {
97
120
  const params = { APIKey: config.apiKey, APIPass: config.apiPass };
98
121
  const res = await ctx.http.get(url, { params });
99
122
  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
- };
123
+ return { valid: true, data: res.TopicInfo, parent: res.ParentInfo, menu: res.Menu || [] };
106
124
  } catch (e) {
107
125
  return { valid: false, msg: "Request Failed" };
108
126
  }
@@ -129,67 +147,40 @@ function apply(ctx, config) {
129
147
  await page.waitForSelector(".card", { timeout: 5e3 });
130
148
  } catch {
131
149
  }
132
- const results = await page.evaluate(() => {
150
+ return await page.evaluate(() => {
133
151
  const items = [];
134
- const cards = document.querySelectorAll(".card.topic-card");
135
- cards.forEach((card) => {
152
+ document.querySelectorAll(".card.topic-card").forEach((card) => {
136
153
  if (items.length >= 6) return;
137
154
  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
- }
155
+ const id = link?.getAttribute("href")?.match(/^\/t\/(\d+)$/)?.[1];
156
+ if (!id || items.some((i) => i.id === id)) return;
157
+ let title = card.querySelector(".card-title")?.textContent?.trim() || link.textContent?.trim() || "";
158
+ let author = card.querySelector('a[href^="/u/"] span.grey-text')?.textContent?.trim() || "";
159
+ let cover = card.querySelector(".card-image img")?.getAttribute("src");
160
+ if (cover && (cover.includes("avatar") && !cover.includes("upload"))) cover = void 0;
156
161
  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) => {
162
+ card.querySelectorAll(".main-tag-set div, .chip").forEach((c) => {
163
163
  const t = c.textContent?.trim();
164
- if (!t) return;
165
- if (["连载中", "已完结", "已弃坑"].includes(t)) status = t;
166
- else if (!t.includes("展开其余")) tags.push(t);
164
+ if (t && !["连载中", "已完结", "已弃坑"].includes(t) && !t.includes("展开")) tags.push(t);
167
165
  });
166
+ const status = Array.from(card.querySelectorAll(".chip")).find((c) => ["连载中", "已完结", "已弃坑"].includes(c.textContent?.trim() || ""))?.textContent?.trim() || "";
168
167
  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";
168
+ card.querySelectorAll(".card-action > div span[title]").forEach((s) => {
169
+ const t = s.getAttribute("title") || "";
170
+ const v = s.textContent?.trim().replace(/[^0-9]/g, "") || "0";
171
+ if (t.includes("")) stats.words = v;
172
+ if (t.includes("阅读")) stats.views = v;
173
+ if (t.includes("评论")) stats.comments = v;
174
+ });
175
+ stats.likes = card.querySelector(".left.green-text")?.textContent?.replace(/[^0-9]/g, "") || "0";
181
176
  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
- }
177
+ const timeTxt = card.querySelector('div[style*="margin: 3px 0;"] span.grey-text')?.textContent || "";
178
+ 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*日)/);
179
+ if (dateMatch) updateTime = dateMatch[1].replace(/\s/g, "");
188
180
  items.push({ id, title, author, cover, tags: tags.slice(0, 8), status, stats, updateTime });
189
181
  });
190
182
  return items;
191
183
  });
192
- return results;
193
184
  } catch (e) {
194
185
  return [];
195
186
  } finally {
@@ -200,18 +191,10 @@ function apply(ctx, config) {
200
191
  const isChapter = info.IsChapter || !!parent && parent.ID !== info.ID;
201
192
  const displayTitle = isChapter && parent ? parent.Title : info.Title;
202
193
  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
- }
194
+ if (isChapter && parent) displayCover = parent.Background || extractImage(parent.Content);
195
+ if (!displayCover) displayCover = info.Background || extractImage(info.Content);
209
196
  const displayTagsObj = isChapter && parent ? parent.Tags : info.Tags;
210
197
  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
198
  const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
216
199
  let summary = stripHtml(info.Content);
217
200
  if (summary.length < 10 && parent && isChapter) summary = stripHtml(parent.Content);
@@ -220,136 +203,73 @@ function apply(ctx, config) {
220
203
  const tagsArr = [];
221
204
  if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
222
205
  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>
206
+ if (displayTagsObj?.OtherTags) tagsArr.push(...displayTagsObj.OtherTags);
207
+ const html = `<!DOCTYPE html><html><head><style>
232
208
  body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
233
209
  .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
210
  .cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
235
211
  .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
212
  .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
- }
213
+ .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; }
214
+ .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
215
  .author { font-size: 13px; color: #888; margin-top: 6px; font-weight: 400; }
251
-
252
216
  .tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 12px; flex-shrink: 0; }
253
217
  .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
218
  .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
- }
219
+ .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; }
220
+ .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
221
  .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>
222
+ </style></head><body>
223
+ <div class="card"><div class="cover"><div class="id-tag">ID: ${info.ID}</div></div>
274
224
  <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>
225
+ <div class="header-group"><div class="title">${displayTitle}</div>${subTitle ? `<div class="subtitle">${subTitle}</div>` : ""}<div class="author">@${info.UserName}</div></div>
226
+ <div class="tags">${tagsArr.slice(0, 10).map((t) => `<span class="tag">${t}</span>`).join("")}</div>
281
227
  <div class="summary-box"><div class="summary">${summary}</div></div>
282
228
  <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>`;
229
+ <span class="stat"><b style="color:#009688">热度</b>${info.Views || 0}</span><span class="stat"><b style="color:#673ab7">评论</b>${info.Comments || 0}</span>
230
+ <span class="stat"><b style="color:#4caf50">赞</b>${info.Upvotes || 0}</span><span class="stat"><b style="color:#795548">字数</b>${info.WordCount || 0}</span>
231
+ </div></div></div></body></html>`;
291
232
  const page = await ctx.puppeteer.page();
292
233
  await injectCookies(page);
293
234
  await page.setContent(html);
294
235
  await page.setViewport({ width: 660, height: 480, deviceScaleFactor: 2 });
295
- const el = await page.$(".card");
296
- const img = await el.screenshot({ type: "png" });
236
+ const img = await page.$(".card").then((e) => e.screenshot({ type: "png" }));
297
237
  await page.close();
298
238
  return img;
299
239
  }, "renderCard");
300
240
  const renderSearchResults = /* @__PURE__ */ __name(async (keyword, results) => {
301
- const html = `
302
- <!DOCTYPE html>
303
- <html>
304
- <head>
305
- <style>
241
+ const html = `<!DOCTYPE html><html><head><style>
306
242
  body { margin: 0; padding: 0; font-family: ${fontStack}; width: 500px; background: transparent; }
307
243
  .container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 10px; }
308
244
  .header { background: #fafafa; padding: 15px 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
309
245
  .header-title { font-size: 16px; font-weight: bold; color: #333; }
310
- .list { padding: 0; }
311
246
  .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; }
247
+ .cover-box { width: 75px; height: 100%; border-radius: 6px; overflow: hidden; flex-shrink: 0; margin-right: 15px; background: #eee; }
313
248
  .cover-img { width: 100%; height: 100%; object-fit: cover; }
314
249
  .content { flex: 1; display: flex; flex-direction: column; justify-content: space-between; height: 100%; min-width: 0; }
315
250
  .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;}
251
+ .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
252
  .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
253
  .author { font-size: 12px; color: #666; }
319
254
  .tags { display: flex; gap: 4px; flex-wrap: wrap; height: 18px; overflow: hidden; margin-top: 4px; }
320
255
  .tag { background: #f3f3f3; color: #666; padding: 0 5px; border-radius: 3px; font-size: 10px; white-space: nowrap; line-height: 1.6;}
321
256
  .meta-row { display: flex; gap: 10px; font-size: 11px; color: #999; margin-top: auto; border-top: 1px dashed #eee; padding-top: 5px; }
322
257
  .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">
258
+ </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
259
  ${results.map((r) => {
330
260
  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
261
  return `<div class="item"><div class="cover-box">${bg}</div><div class="content">
338
262
  <div class="top-row"><div class="title">${r.title}</div><div class="id-badge">ID: ${r.id}</div></div>
339
263
  <div class="author">By ${r.author} ${r.status ? ` · ${r.status}` : ""}</div>
340
264
  <div class="tags">${r.tags.map((t) => `<span class="tag">${t}</span>`).join("")}</div>
341
- <div class="meta-row">${stats || "No Data"}</div>
265
+ <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
266
  </div></div>`;
343
267
  }).join("")}
344
- </div>
345
- </div>
346
- </body></html>`;
268
+ </div></div></body></html>`;
347
269
  const page = await ctx.puppeteer.page();
348
270
  await page.setContent(html);
349
271
  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" });
272
+ const img = await page.$(".container").then((e) => e.screenshot({ type: "png" }));
353
273
  await page.close();
354
274
  return img;
355
275
  }, "renderSearchResults");
@@ -359,7 +279,7 @@ function apply(ctx, config) {
359
279
  const footerHeight = 30;
360
280
  const paddingX = 25;
361
281
  const paddingY = 20;
362
- const lineHeightRatio = 1.8;
282
+ const lineHeightRatio = 1.6;
363
283
  const contentWidth = config.deviceWidth - paddingX * 2;
364
284
  const columnGap = 40;
365
285
  const maxContentHeight = config.deviceHeight - headerHeight - footerHeight;
@@ -367,83 +287,60 @@ function apply(ctx, config) {
367
287
  const linesPerPage = Math.floor((maxContentHeight - paddingY * 2) / lineHeightPx);
368
288
  const optimalContentHeight = linesPerPage * lineHeightPx + paddingY * 2;
369
289
  const marginTop = Math.floor((maxContentHeight - optimalContentHeight) / 2) + headerHeight;
370
- const html = `
371
- <!DOCTYPE html>
372
- <html>
373
- <head>
374
- <style>
290
+ const html = `<!DOCTYPE html><html><head><style>
375
291
  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;}
292
+ .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; }
293
+ .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; }
294
+ #viewport { position: absolute; top: ${marginTop}px; left: ${paddingX}px; width: ${contentWidth}px; height: ${optimalContentHeight}px; overflow: hidden; }
295
+ #content-scroller { height: 100%; width: 100%; column-width: ${contentWidth}px; column-gap: ${columnGap}px; column-fill: auto; padding: ${paddingY}px 0; box-sizing: border-box; font-size: ${config.fontSize}px; line-height: ${lineHeightRatio}; text-align: left; transform: translateX(0); transition: none; }
376
296
 
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;
382
- }
297
+ /* 基础文本 */
298
+ p, div { margin: 0 0 0.2em 0; text-indent: 2em; word-wrap: break-word; overflow-wrap: break-word; }
383
299
 
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
- }
300
+ /* 辅助类 */
301
+ .align-center { text-align: center !important; text-align-last: center !important; text-indent: 0 !important; margin: 0.8em 0; font-weight: bold; color: #5d4037; }
302
+ .align-right { text-align: right !important; text-indent: 0 !important; margin-top: 0.5em; color: #666; font-style: italic; }
303
+ .no-indent { text-indent: 0 !important; }
420
304
 
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; }
305
+ /* 富文本支持 */
306
+ blockquote { margin: 1em 0.5em; padding-left: 1em; border-left: 4px solid #d7ccc8; color: #666; }
307
+ blockquote p { text-indent: 0; margin: 0.3em 0; }
308
+
309
+ ul, ol { margin: 0.5em 0; padding-left: 1.5em; }
310
+ li { margin-bottom: 0.2em; }
428
311
 
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>
312
+ hr { border: 0; height: 1px; background: #d7ccc8; margin: 1.5em 0; }
313
+
314
+ table { width: 100%; border-collapse: collapse; margin: 1em 0; font-size: 0.9em; }
315
+ th, td { border: 1px solid #ccc; padding: 4px; text-align: left; }
316
+ th { background: #eee; font-weight: bold; }
317
+
318
+ pre { background: #eee; padding: 0.5em; overflow-x: auto; border-radius: 4px; margin: 0.5em 0; }
319
+ code { font-family: monospace; background: #f0f0f0; padding: 2px 4px; border-radius: 3px; }
320
+
321
+ s, strike, del { text-decoration: line-through; color: #888; }
322
+ u { text-decoration: underline; }
323
+ sup, sub { font-size: 0.75em; line-height: 0; position: relative; vertical-align: baseline; }
324
+ sup { top: -0.5em; }
325
+ sub { bottom: -0.25em; }
326
+
327
+ a { color: #0277bd; text-decoration: none; }
444
328
 
445
- <div class="fixed-footer" id="page-indicator">- 1 -</div>
446
- </body></html>`;
329
+ /* 图片 */
330
+ .img-box { display: flex; justify-content: center; align-items: center; margin: 0.5em 0; width: 100%; }
331
+ img { max-width: 100%; height: auto; display: block; border-radius: 6px; box-shadow: 0 2px 6px rgba(0,0,0,0.1); }
332
+
333
+ /* 标题 */
334
+ 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; }
335
+
336
+ strong, b { font-weight: 900; color: #3e2723; }
337
+ em, i { font-style: italic; }
338
+
339
+ p:last-child { margin-bottom: 0; }
340
+ </style></head><body>
341
+ <div class="fixed-header"><span>${info.Title.substring(0, 12) + (info.Title.length > 12 ? "..." : "")}</span><span>${info.UserName}</span></div>
342
+ <div id="viewport"><div id="content-scroller">${content}</div></div>
343
+ <div class="fixed-footer" id="page-indicator">- 1 -</div></body></html>`;
447
344
  const page = await ctx.puppeteer.page();
448
345
  try {
449
346
  await injectCookies(page);
@@ -460,8 +357,7 @@ function apply(ctx, config) {
460
357
  document.getElementById("content-scroller").style.transform = `translateX(${offset}px)`;
461
358
  document.getElementById("page-indicator").innerText = `- ${curr} / ${total} -`;
462
359
  }, i, step, i + 1, finalPages);
463
- const img = await page.screenshot({ type: "jpeg", quality: 80 });
464
- imgs.push(img);
360
+ imgs.push(await page.screenshot({ type: "jpeg", quality: 80 }));
465
361
  }
466
362
  return imgs;
467
363
  } finally {
@@ -472,8 +368,7 @@ function apply(ctx, config) {
472
368
  if (!threadId) return "请输入ID";
473
369
  const res = await fetchThread(threadId);
474
370
  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"));
371
+ return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
477
372
  });
478
373
  ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
479
374
  if (!threadId) return "请输入ID";
@@ -503,8 +398,7 @@ function apply(ctx, config) {
503
398
  if (!id) return "[错误] 获取失败";
504
399
  const res = await fetchThread(id);
505
400
  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"));
401
+ await session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
508
402
  return `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
509
403
  });
510
404
  ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
@@ -512,8 +406,7 @@ function apply(ctx, config) {
512
406
  await session.send("[加载中] 搜索中...");
513
407
  const results = await searchThreads(keyword);
514
408
  if (!results.length) return "未找到结果。";
515
- const img = await renderSearchResults(keyword, results);
516
- await session.send(import_koishi.h.image(img, "image/png"));
409
+ await session.send(import_koishi.h.image(await renderSearchResults(keyword, results), "image/png"));
517
410
  const exampleId = results[0]?.id || "12345";
518
411
  return `Tip: 发送 /ft.read [ID] 阅读 (例: /ft.read ${exampleId})`;
519
412
  });
@@ -525,8 +418,7 @@ function apply(ctx, config) {
525
418
  if (!res.valid) return "帖子不存在";
526
419
  await ctx.database.create("fimtale_subs", { cid: session.cid, threadId, lastCount: res.data.Comments, lastCheck: Date.now() });
527
420
  await session.send("[成功] 订阅成功");
528
- const img = await renderCard(res.data, res.parent);
529
- return session.send(import_koishi.h.image(img, "image/png"));
421
+ return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
530
422
  });
531
423
  ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
532
424
  const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
@@ -538,8 +430,7 @@ function apply(ctx, config) {
538
430
  if (match && match[1] && session.userId !== session.selfId) {
539
431
  const res = await fetchThread(match[1]);
540
432
  if (res.valid) {
541
- const img = await renderCard(res.data, res.parent);
542
- session.send(import_koishi.h.image(img, "image/png"));
433
+ session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/png"));
543
434
  }
544
435
  }
545
436
  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.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [