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.
- package/lib/index.js +156 -234
- 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
|
-
|
|
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(/ /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
|
-
|
|
153
|
+
return await page.evaluate(() => {
|
|
133
154
|
const items = [];
|
|
134
|
-
|
|
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
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
|
183
|
-
|
|
184
|
-
|
|
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
|
-
|
|
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
|
|
224
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>${
|
|
284
|
-
<span class="stat"><b style="color:#
|
|
285
|
-
|
|
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
|
|
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;
|
|
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;
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
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
|
-
|
|
385
|
-
|
|
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
|
-
/*
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
/*
|
|
427
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|