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.
- package/lib/index.js +126 -235
- 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
|
-
|
|
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(/ /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
|
-
|
|
150
|
+
return await page.evaluate(() => {
|
|
133
151
|
const items = [];
|
|
134
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
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)
|
|
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
|
-
|
|
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";
|
|
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
|
|
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
|
-
}
|
|
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
|
-
|
|
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
|
|
224
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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>${
|
|
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>`;
|
|
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
|
|
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;
|
|
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;
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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
|
-
/*
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
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
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
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
|
-
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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();
|