koishi-plugin-fimtale-api 1.0.7 → 1.0.9
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.d.ts +1 -0
- package/lib/index.js +91 -32
- package/package.json +1 -1
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -49,9 +49,16 @@ var Config = import_koishi.Schema.object({
|
|
|
49
49
|
autoParseLink: import_koishi.Schema.boolean().default(true).description("自动解析链接为预览卡片"),
|
|
50
50
|
deviceWidth: import_koishi.Schema.number().default(390).description("阅读器渲染宽度(px)"),
|
|
51
51
|
deviceHeight: import_koishi.Schema.number().default(844).description("阅读器渲染高度(px)"),
|
|
52
|
-
fontSize: import_koishi.Schema.number().default(20).description("正文字号(px)")
|
|
52
|
+
fontSize: import_koishi.Schema.number().default(20).description("正文字号(px)"),
|
|
53
|
+
debug: import_koishi.Schema.boolean().default(false).description("开启详细调试日志")
|
|
53
54
|
});
|
|
54
55
|
function apply(ctx, config) {
|
|
56
|
+
const logger = ctx.logger("fimtale");
|
|
57
|
+
const debugLog = /* @__PURE__ */ __name((msg, ...args) => {
|
|
58
|
+
if (config.debug) {
|
|
59
|
+
logger.info(`[DEBUG] ${msg}`, ...args);
|
|
60
|
+
}
|
|
61
|
+
}, "debugLog");
|
|
55
62
|
ctx.model.extend("fimtale_subs", {
|
|
56
63
|
id: "unsigned",
|
|
57
64
|
cid: "string",
|
|
@@ -109,48 +116,67 @@ function apply(ctx, config) {
|
|
|
109
116
|
const fontStack = '"Noto Sans SC", "Microsoft YaHei", "PingFang SC", sans-serif';
|
|
110
117
|
const fontSerif = '"Noto Serif SC", "Source Han Serif SC", "SimSun", serif';
|
|
111
118
|
const injectCookies = /* @__PURE__ */ __name(async (page) => {
|
|
112
|
-
if (!config.cookies)
|
|
119
|
+
if (!config.cookies) {
|
|
120
|
+
debugLog("No cookies configured, skipping injection.");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
113
123
|
const cookies = config.cookies.split(";").map((pair) => {
|
|
114
124
|
const parts = pair.trim().split("=");
|
|
115
125
|
if (parts.length < 2) return null;
|
|
116
126
|
return { name: parts[0].trim(), value: parts.slice(1).join("=").trim(), domain: "fimtale.com", path: "/" };
|
|
117
127
|
}).filter((c) => c !== null);
|
|
118
|
-
if (cookies.length)
|
|
128
|
+
if (cookies.length) {
|
|
129
|
+
debugLog(`Injecting ${cookies.length} cookies into Puppeteer page.`);
|
|
130
|
+
await page.setCookie(...cookies);
|
|
131
|
+
}
|
|
119
132
|
}, "injectCookies");
|
|
120
133
|
const fetchThread = /* @__PURE__ */ __name(async (threadId) => {
|
|
134
|
+
const startTime = Date.now();
|
|
121
135
|
try {
|
|
122
136
|
const url = `${config.apiUrl}/t/${threadId}`;
|
|
137
|
+
debugLog(`Fetching thread info: ${url}`);
|
|
123
138
|
const params = { APIKey: config.apiKey, APIPass: config.apiPass };
|
|
124
139
|
const res = await ctx.http.get(url, { params });
|
|
125
|
-
|
|
140
|
+
debugLog(`Fetch thread ${threadId} completed in ${Date.now() - startTime}ms. Status: ${res.Status}`);
|
|
141
|
+
if (res.Status !== 1 || !res.TopicInfo) {
|
|
142
|
+
debugLog(`Fetch error for ${threadId}: ${res.ErrorMessage}`);
|
|
143
|
+
return { valid: false, msg: res.ErrorMessage || "API Error" };
|
|
144
|
+
}
|
|
126
145
|
return { valid: true, data: res.TopicInfo, parent: res.ParentInfo, menu: res.Menu || [] };
|
|
127
146
|
} catch (e) {
|
|
147
|
+
logger.error(`Failed to fetch thread ${threadId}:`, e);
|
|
128
148
|
return { valid: false, msg: "Request Failed" };
|
|
129
149
|
}
|
|
130
150
|
}, "fetchThread");
|
|
131
151
|
const fetchRandomId = /* @__PURE__ */ __name(async () => {
|
|
132
152
|
try {
|
|
153
|
+
debugLog("Fetching random thread ID...");
|
|
133
154
|
const headers = config.cookies ? { Cookie: config.cookies } : {};
|
|
134
155
|
const html = await ctx.http.get("https://fimtale.com/rand", { responseType: "text", headers });
|
|
135
156
|
let match = html.match(/FimTale\.topic\.init\((\d+)/) || html.match(/data-clipboard-text=".*?\/t\/(\d+)"/);
|
|
136
|
-
|
|
157
|
+
const result = match ? match[1] : null;
|
|
158
|
+
debugLog(`Random ID result: ${result}`);
|
|
159
|
+
return result;
|
|
137
160
|
} catch (e) {
|
|
161
|
+
logger.error("Failed to fetch random ID:", e);
|
|
138
162
|
return null;
|
|
139
163
|
}
|
|
140
164
|
}, "fetchRandomId");
|
|
141
165
|
const searchThreads = /* @__PURE__ */ __name(async (keyword) => {
|
|
142
166
|
let page;
|
|
167
|
+
debugLog(`Starting search for keyword: "${keyword}"`);
|
|
143
168
|
try {
|
|
144
169
|
page = await ctx.puppeteer.page();
|
|
145
170
|
await page.setUserAgent("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36");
|
|
146
171
|
await injectCookies(page);
|
|
147
172
|
const searchUrl = `https://fimtale.com/topics?q=${encodeURIComponent(keyword)}`;
|
|
173
|
+
debugLog(`Navigating to: ${searchUrl}`);
|
|
148
174
|
await page.goto(searchUrl, { waitUntil: "networkidle2", timeout: 25e3 });
|
|
149
175
|
try {
|
|
150
176
|
await page.waitForSelector(".card", { timeout: 5e3 });
|
|
151
177
|
} catch {
|
|
152
178
|
}
|
|
153
|
-
|
|
179
|
+
const results = await page.evaluate(() => {
|
|
154
180
|
const items = [];
|
|
155
181
|
document.querySelectorAll(".card.topic-card").forEach((card) => {
|
|
156
182
|
if (items.length >= 6) return;
|
|
@@ -184,24 +210,48 @@ function apply(ctx, config) {
|
|
|
184
210
|
});
|
|
185
211
|
return items;
|
|
186
212
|
});
|
|
213
|
+
debugLog(`Search found ${results.length} items.`);
|
|
214
|
+
return results;
|
|
187
215
|
} catch (e) {
|
|
216
|
+
logger.error("Search error:", e);
|
|
188
217
|
return [];
|
|
189
218
|
} finally {
|
|
190
|
-
if (page)
|
|
219
|
+
if (page) {
|
|
220
|
+
await page.close();
|
|
221
|
+
debugLog("Search page closed.");
|
|
222
|
+
}
|
|
191
223
|
}
|
|
192
224
|
}, "searchThreads");
|
|
193
225
|
const renderCard = /* @__PURE__ */ __name(async (info, parent) => {
|
|
226
|
+
debugLog(`Rendering Card for ID: ${info.ID}`);
|
|
194
227
|
const isChapter = info.IsChapter || !!parent && parent.ID !== info.ID;
|
|
195
228
|
const displayTitle = isChapter && parent ? parent.Title : info.Title;
|
|
196
229
|
let displayCover = info.Background || extractImage(info.Content);
|
|
197
230
|
if (!displayCover && parent) {
|
|
198
|
-
displayCover = parent.Background
|
|
231
|
+
displayCover = parent.Background;
|
|
232
|
+
if (!displayCover) {
|
|
233
|
+
if (parent.Content) {
|
|
234
|
+
displayCover = extractImage(parent.Content);
|
|
235
|
+
} else {
|
|
236
|
+
try {
|
|
237
|
+
debugLog(`Missing parent content/cover for chapter ${info.ID}, fetching parent ${parent.ID}...`);
|
|
238
|
+
const parentRes = await fetchThread(parent.ID.toString());
|
|
239
|
+
if (parentRes.valid && parentRes.data) {
|
|
240
|
+
displayCover = parentRes.data.Background || extractImage(parentRes.data.Content);
|
|
241
|
+
}
|
|
242
|
+
} catch (e) {
|
|
243
|
+
debugLog("Fetch parent for cover failed (non-critical).");
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
199
247
|
}
|
|
200
248
|
const displayTagsObj = isChapter && parent ? parent.Tags : info.Tags;
|
|
201
249
|
const subTitle = isChapter ? info.Title : null;
|
|
202
250
|
const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
|
|
203
251
|
let summary = stripHtml(info.Content);
|
|
204
|
-
if (summary.length < 10 && parent && isChapter)
|
|
252
|
+
if (summary.length < 10 && parent && isChapter) {
|
|
253
|
+
summary = stripHtml(parent.Content);
|
|
254
|
+
}
|
|
205
255
|
if (summary.length > 150) summary = summary.substring(0, 150) + "...";
|
|
206
256
|
if (!summary) summary = "暂无简介";
|
|
207
257
|
const tagsArr = [];
|
|
@@ -213,8 +263,6 @@ function apply(ctx, config) {
|
|
|
213
263
|
body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
|
|
214
264
|
.card { width: 620px; min-height: 420px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
|
|
215
265
|
.cover { width: 220px; min-height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
|
|
216
|
-
|
|
217
|
-
/* ID Badge 分体式:使用 flex 强制垂直居中,消除字体基线差异 */
|
|
218
266
|
.id-badge-container {
|
|
219
267
|
position: absolute; top: 15px; left: 15px;
|
|
220
268
|
display: flex;
|
|
@@ -222,7 +270,7 @@ function apply(ctx, config) {
|
|
|
222
270
|
border-radius: 6px;
|
|
223
271
|
overflow: hidden;
|
|
224
272
|
border: 1px solid rgba(255,255,255,0.3);
|
|
225
|
-
height: 28px;
|
|
273
|
+
height: 28px;
|
|
226
274
|
}
|
|
227
275
|
.id-label {
|
|
228
276
|
background: #EE6E73;
|
|
@@ -234,7 +282,7 @@ function apply(ctx, config) {
|
|
|
234
282
|
text-transform: uppercase;
|
|
235
283
|
display: flex; align-items: center; justify-content: center;
|
|
236
284
|
height: 100%;
|
|
237
|
-
line-height: 1; margin: 0;
|
|
285
|
+
line-height: 1; margin: 0;
|
|
238
286
|
}
|
|
239
287
|
.id-val {
|
|
240
288
|
background: #fff;
|
|
@@ -245,23 +293,17 @@ function apply(ctx, config) {
|
|
|
245
293
|
font-weight: 900;
|
|
246
294
|
display: flex; align-items: center; justify-content: center;
|
|
247
295
|
height: 100%;
|
|
248
|
-
line-height: 1; margin: 0;
|
|
296
|
+
line-height: 1; margin: 0;
|
|
249
297
|
}
|
|
250
|
-
|
|
251
298
|
.info { flex: 1; padding: 26px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
|
|
252
|
-
|
|
253
299
|
.header-group { flex-shrink: 0; margin-bottom: 16px; border-bottom: 2px solid #f5f5f5; padding-bottom: 12px; }
|
|
254
300
|
.title { font-size: 24px; font-weight: 700; color: #333; line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; margin-bottom: 6px; }
|
|
255
301
|
.subtitle { font-size: 16px; color: #78909C; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; padding-left: 12px; border-left: 4px solid #EE6E73; margin-top: 6px; }
|
|
256
|
-
|
|
257
302
|
.author { font-size: 14px; color: #78909C; margin-top: 12px; font-weight: 400; display:flex; align-items:center; }
|
|
258
|
-
|
|
259
303
|
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; flex-shrink: 0; }
|
|
260
304
|
.tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
|
|
261
|
-
|
|
262
305
|
.summary-box { flex: 1; position: relative; overflow: hidden; min-height: 0; margin-bottom: 16px; }
|
|
263
306
|
.summary { font-size: 14px; color: #546e7a; line-height: 1.7; display: -webkit-box; -webkit-line-clamp: 6; -webkit-box-orient: vertical; overflow: hidden; text-align: justify; }
|
|
264
|
-
|
|
265
307
|
.footer { border-top: 1px solid #eee; padding-top: 14px; display: flex; justify-content: space-between; font-size: 13px; color: #78909C; margin-top: auto; flex-shrink: 0; }
|
|
266
308
|
.stat b { color: #455a64; font-weight: bold; margin-right: 3px;}
|
|
267
309
|
</style></head><body>
|
|
@@ -280,18 +322,24 @@ function apply(ctx, config) {
|
|
|
280
322
|
<span class="stat"><b style="color:#009688">热度</b>${info.Views || 0}</span><span class="stat"><b style="color:#7e57c2">评论</b>${info.Comments || 0}</span>
|
|
281
323
|
<span class="stat"><b style="color:#4caf50">赞</b>${likes}</span><span class="stat"><b style="color:#8d6e63">字数</b>${info.WordCount || 0}</span>
|
|
282
324
|
</div></div></div></body></html>`;
|
|
325
|
+
debugLog(`Card HTML prepared (approx ${html.length} chars). Launching Puppeteer...`);
|
|
283
326
|
const page = await ctx.puppeteer.page();
|
|
284
327
|
try {
|
|
285
328
|
await injectCookies(page);
|
|
286
329
|
await page.setContent(html);
|
|
287
330
|
await page.setViewport({ width: 660, height: 480, deviceScaleFactor: 3 });
|
|
288
331
|
const img = await page.$(".card").then((e) => e.screenshot({ type: "jpeg", quality: 100 }));
|
|
332
|
+
debugLog("Card screenshot captured successfully.");
|
|
289
333
|
return img;
|
|
334
|
+
} catch (e) {
|
|
335
|
+
logger.error("Error rendering card:", e);
|
|
336
|
+
throw e;
|
|
290
337
|
} finally {
|
|
291
338
|
await page.close();
|
|
292
339
|
}
|
|
293
340
|
}, "renderCard");
|
|
294
341
|
const renderSearchResults = /* @__PURE__ */ __name(async (keyword, results) => {
|
|
342
|
+
debugLog(`Rendering search results: ${results.length} items`);
|
|
295
343
|
const html = `<!DOCTYPE html><html><head><style>
|
|
296
344
|
body { margin: 0; padding: 0; font-family: ${fontStack}; width: 500px; background: transparent; }
|
|
297
345
|
.container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 10px; }
|
|
@@ -304,8 +352,6 @@ function apply(ctx, config) {
|
|
|
304
352
|
.content { flex: 1; display: flex; flex-direction: column; justify-content: space-between; height: 100%; min-width: 0; }
|
|
305
353
|
.top-row { display: flex; justify-content: space-between; align-items: flex-start; }
|
|
306
354
|
.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;}
|
|
307
|
-
|
|
308
|
-
/* ID Badge Search: 迷你分体式,保持与大图风格一致但更紧凑 */
|
|
309
355
|
.id-badge {
|
|
310
356
|
display: flex;
|
|
311
357
|
border-radius: 4px;
|
|
@@ -323,7 +369,7 @@ function apply(ctx, config) {
|
|
|
323
369
|
font-weight: bold;
|
|
324
370
|
display: flex; align-items: center; justify-content: center;
|
|
325
371
|
height: 100%;
|
|
326
|
-
line-height: 1; margin: 0;
|
|
372
|
+
line-height: 1; margin: 0;
|
|
327
373
|
}
|
|
328
374
|
.id-val {
|
|
329
375
|
background: #fff;
|
|
@@ -334,9 +380,8 @@ function apply(ctx, config) {
|
|
|
334
380
|
font-weight: bold;
|
|
335
381
|
display: flex; align-items: center; justify-content: center;
|
|
336
382
|
height: 100%;
|
|
337
|
-
line-height: 1; margin: 0;
|
|
383
|
+
line-height: 1; margin: 0;
|
|
338
384
|
}
|
|
339
|
-
|
|
340
385
|
.author { font-size: 12px; color: #78909C; }
|
|
341
386
|
.tags { display: flex; gap: 4px; flex-wrap: wrap; height: 18px; overflow: hidden; margin-top: 4px; }
|
|
342
387
|
.tag { background: #f3f3f3; color: #666; padding: 0 5px; border-radius: 3px; font-size: 10px; white-space: nowrap; line-height: 1.6;}
|
|
@@ -363,12 +408,14 @@ function apply(ctx, config) {
|
|
|
363
408
|
await page.setContent(html);
|
|
364
409
|
await page.setViewport({ width: 550, height: 800, deviceScaleFactor: 3 });
|
|
365
410
|
const img = await page.$(".container").then((e) => e.screenshot({ type: "jpeg", quality: 100 }));
|
|
411
|
+
debugLog("Search results screenshot taken.");
|
|
366
412
|
return img;
|
|
367
413
|
} finally {
|
|
368
414
|
await page.close();
|
|
369
415
|
}
|
|
370
416
|
}, "renderSearchResults");
|
|
371
417
|
const renderReadPages = /* @__PURE__ */ __name(async (info) => {
|
|
418
|
+
debugLog(`Rendering read pages for ${info.ID} (${info.Title})`);
|
|
372
419
|
const content = cleanContent(info.Content);
|
|
373
420
|
const headerHeight = 40;
|
|
374
421
|
const footerHeight = 30;
|
|
@@ -384,10 +431,8 @@ function apply(ctx, config) {
|
|
|
384
431
|
const marginTop = Math.floor((maxContentHeight - optimalContentHeight) / 2) + headerHeight;
|
|
385
432
|
const html = `<!DOCTYPE html><html><head><style>
|
|
386
433
|
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;}
|
|
387
|
-
/* Header Padding reduced to 12px to align closer to left edge */
|
|
388
434
|
.fixed-header { position: absolute; top: 0; left: 0; width: 100%; height: ${headerHeight}px; border-bottom: 2px solid #EE6E73; box-sizing: border-box; padding: 0 12px; display: flex; align-items: center; justify-content: space-between; font-size: 12px; color: #EE6E73; background: #f6f4ec; z-index: 5; font-weight: bold; }
|
|
389
435
|
.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: #78909C; background: #f6f4ec; z-index: 5; }
|
|
390
|
-
/* 修复1: 强制重置 header 和 footer 的缩进,防止 p, div 全局规则影响 */
|
|
391
436
|
.fixed-header, .fixed-footer, .header-title, .header-author { text-indent: 0 !important; }
|
|
392
437
|
|
|
393
438
|
#viewport { position: absolute; top: ${marginTop}px; left: ${paddingX}px; width: ${contentWidth}px; height: ${optimalContentHeight}px; overflow: hidden; }
|
|
@@ -397,11 +442,8 @@ function apply(ctx, config) {
|
|
|
397
442
|
.align-center { text-align: center !important; text-align-last: center !important; text-indent: 0 !important; margin: 0.8em 0; font-weight: bold; color: #5d4037; }
|
|
398
443
|
.align-right { text-align: right !important; text-indent: 0 !important; margin-top: 0.5em; color: #666; font-style: italic; }
|
|
399
444
|
.no-indent { text-indent: 0 !important; }
|
|
400
|
-
|
|
401
|
-
/* Header Title absolute left */
|
|
402
445
|
.header-title { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: left; min-width: 0; margin-right: 10px; }
|
|
403
446
|
.header-author { flex-shrink: 0; color: #78909C; max-width: 35%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
|
|
404
|
-
|
|
405
447
|
blockquote { margin: 1em 0.5em; padding-left: 1em; border-left: 4px solid #EE6E73; color: #666; }
|
|
406
448
|
blockquote p { text-indent: 0; margin: 0.3em 0; }
|
|
407
449
|
ul, ol { margin: 0.5em 0; padding-left: 1.5em; }
|
|
@@ -436,6 +478,7 @@ function apply(ctx, config) {
|
|
|
436
478
|
await injectCookies(page);
|
|
437
479
|
await page.setContent(html);
|
|
438
480
|
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 3 });
|
|
481
|
+
debugLog("Waiting for fonts and images...");
|
|
439
482
|
await page.evaluate(async () => {
|
|
440
483
|
await document.fonts.ready;
|
|
441
484
|
await new Promise((resolve) => {
|
|
@@ -460,6 +503,7 @@ function apply(ctx, config) {
|
|
|
460
503
|
const step = contentWidth + columnGap;
|
|
461
504
|
const totalPages = Math.floor((scrollWidth + columnGap - 10) / step) + 1;
|
|
462
505
|
const finalPages = Math.max(1, totalPages);
|
|
506
|
+
debugLog(`Content width calculated: ${scrollWidth}px. Splitting into ${finalPages} pages.`);
|
|
463
507
|
const imgs = [];
|
|
464
508
|
for (let i = 0; i < finalPages; i++) {
|
|
465
509
|
await page.evaluate((idx, stepPx, curr, total) => {
|
|
@@ -469,18 +513,24 @@ function apply(ctx, config) {
|
|
|
469
513
|
}, i, step, i + 1, finalPages);
|
|
470
514
|
imgs.push(await page.screenshot({ type: "jpeg", quality: 100 }));
|
|
471
515
|
}
|
|
516
|
+
debugLog("All pages captured.");
|
|
472
517
|
return imgs;
|
|
518
|
+
} catch (e) {
|
|
519
|
+
logger.error("Error rendering read pages:", e);
|
|
520
|
+
throw e;
|
|
473
521
|
} finally {
|
|
474
522
|
await page.close();
|
|
475
523
|
}
|
|
476
524
|
}, "renderReadPages");
|
|
477
525
|
ctx.command("ft.info <threadId:string>", "预览作品").action(async ({ session }, threadId) => {
|
|
526
|
+
debugLog(`CMD ft.info triggered by ${session.userId} for ID: ${threadId}`);
|
|
478
527
|
if (!threadId) return "请输入ID";
|
|
479
528
|
const res = await fetchThread(threadId);
|
|
480
529
|
if (!res.valid) return `[错误] ${res.msg}`;
|
|
481
530
|
return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
|
|
482
531
|
});
|
|
483
532
|
ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
|
|
533
|
+
debugLog(`CMD ft.read triggered by ${session.userId} for ID: ${threadId}`);
|
|
484
534
|
if (!threadId) return "请输入ID";
|
|
485
535
|
const res = await fetchThread(threadId);
|
|
486
536
|
if (!res.valid) return `[错误] 读取失败: ${res.msg}`;
|
|
@@ -501,11 +551,12 @@ function apply(ctx, config) {
|
|
|
501
551
|
if (navs.length) nodes.push((0, import_koishi.h)("message", import_koishi.h.text("章节导航:\n" + navs.join("\n"))));
|
|
502
552
|
return session.send((0, import_koishi.h)("message", { forward: true }, nodes));
|
|
503
553
|
} catch (e) {
|
|
504
|
-
|
|
554
|
+
logger.error("ft.read rendering failed:", e);
|
|
505
555
|
return "[错误] 渲染失败";
|
|
506
556
|
}
|
|
507
557
|
});
|
|
508
558
|
ctx.command("ft.random", "随机作品").action(async ({ session }) => {
|
|
559
|
+
debugLog(`CMD ft.random triggered by ${session.userId}`);
|
|
509
560
|
const id = await fetchRandomId();
|
|
510
561
|
if (!id) return "[错误] 获取失败";
|
|
511
562
|
const res = await fetchThread(id);
|
|
@@ -514,6 +565,7 @@ function apply(ctx, config) {
|
|
|
514
565
|
return `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
|
|
515
566
|
});
|
|
516
567
|
ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
|
|
568
|
+
debugLog(`CMD ft.search triggered by ${session.userId} for "${keyword}"`);
|
|
517
569
|
if (!keyword) return "请输入关键词";
|
|
518
570
|
await session.send("[加载中] 搜索中...");
|
|
519
571
|
const results = await searchThreads(keyword);
|
|
@@ -523,6 +575,7 @@ function apply(ctx, config) {
|
|
|
523
575
|
return `Tip: 发送 /ft.read [ID] 阅读 (例: /ft.read ${exampleId})`;
|
|
524
576
|
});
|
|
525
577
|
ctx.command("ft.sub <threadId:string>", "订阅").action(async ({ session }, threadId) => {
|
|
578
|
+
debugLog(`CMD ft.sub triggered by ${session.userId} for ID: ${threadId}`);
|
|
526
579
|
if (!/^\d+$/.test(threadId)) return "ID错误";
|
|
527
580
|
const exist = await ctx.database.get("fimtale_subs", { cid: session.cid, threadId });
|
|
528
581
|
if (exist.length) return "已订阅";
|
|
@@ -533,6 +586,7 @@ function apply(ctx, config) {
|
|
|
533
586
|
return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
|
|
534
587
|
});
|
|
535
588
|
ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
|
|
589
|
+
debugLog(`CMD ft.unsub triggered by ${session.userId} for ID: ${threadId}`);
|
|
536
590
|
const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
|
|
537
591
|
return res.matched ? "[成功] 已退订" : "未找到订阅";
|
|
538
592
|
});
|
|
@@ -540,6 +594,7 @@ function apply(ctx, config) {
|
|
|
540
594
|
if (!config.autoParseLink) return next();
|
|
541
595
|
const matches = [...session.content.matchAll(/fimtale\.(?:com|net)\/t\/(\d+)/g)];
|
|
542
596
|
if (matches.length === 0) return next();
|
|
597
|
+
debugLog(`Middleware matched link in channel ${session.channelId}`);
|
|
543
598
|
const uniqueIds = [...new Set(matches.map((m) => m[1]))];
|
|
544
599
|
if (session.userId === session.selfId) return next();
|
|
545
600
|
const messageNodes = [];
|
|
@@ -551,6 +606,7 @@ function apply(ctx, config) {
|
|
|
551
606
|
messageNodes.push((0, import_koishi.h)("message", import_koishi.h.image(img, "image/jpeg")));
|
|
552
607
|
}
|
|
553
608
|
} catch (e) {
|
|
609
|
+
logger.error(`Middleware failed to process link ${id}:`, e);
|
|
554
610
|
}
|
|
555
611
|
}
|
|
556
612
|
if (messageNodes.length === 0) return next();
|
|
@@ -563,12 +619,14 @@ function apply(ctx, config) {
|
|
|
563
619
|
ctx.setInterval(async () => {
|
|
564
620
|
const subs = await ctx.database.get("fimtale_subs", {});
|
|
565
621
|
if (!subs.length) return;
|
|
622
|
+
debugLog(`Polling check started for ${subs.length} subscriptions.`);
|
|
566
623
|
const tids = [...new Set(subs.map((s) => s.threadId))];
|
|
567
624
|
for (const tid of tids) {
|
|
568
625
|
const res = await fetchThread(tid);
|
|
569
626
|
if (!res.valid) continue;
|
|
570
627
|
const targets = subs.filter((s) => s.threadId === tid && s.lastCount < res.data.Comments);
|
|
571
628
|
if (targets.length) {
|
|
629
|
+
debugLog(`Update found for thread ${tid} (Old: ${targets[0].lastCount}, New: ${res.data.Comments})`);
|
|
572
630
|
const msg = `[更新] ${res.data.Title} 更新了!
|
|
573
631
|
回复: ${res.data.Comments}
|
|
574
632
|
https://fimtale.com/t/${tid}`;
|
|
@@ -576,7 +634,8 @@ https://fimtale.com/t/${tid}`;
|
|
|
576
634
|
try {
|
|
577
635
|
await ctx.broadcast([sub.cid], import_koishi.h.parse(msg));
|
|
578
636
|
await ctx.database.set("fimtale_subs", { id: sub.id }, { lastCount: res.data.Comments });
|
|
579
|
-
} catch {
|
|
637
|
+
} catch (e) {
|
|
638
|
+
logger.error(`Broadcast failed for sub ${sub.id}:`, e);
|
|
580
639
|
}
|
|
581
640
|
}
|
|
582
641
|
}
|