koishi-plugin-fimtale-api 1.0.8 → 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 +74 -30
- 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,13 +210,20 @@ 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);
|
|
@@ -201,11 +234,13 @@ function apply(ctx, config) {
|
|
|
201
234
|
displayCover = extractImage(parent.Content);
|
|
202
235
|
} else {
|
|
203
236
|
try {
|
|
237
|
+
debugLog(`Missing parent content/cover for chapter ${info.ID}, fetching parent ${parent.ID}...`);
|
|
204
238
|
const parentRes = await fetchThread(parent.ID.toString());
|
|
205
239
|
if (parentRes.valid && parentRes.data) {
|
|
206
240
|
displayCover = parentRes.data.Background || extractImage(parentRes.data.Content);
|
|
207
241
|
}
|
|
208
242
|
} catch (e) {
|
|
243
|
+
debugLog("Fetch parent for cover failed (non-critical).");
|
|
209
244
|
}
|
|
210
245
|
}
|
|
211
246
|
}
|
|
@@ -228,8 +263,6 @@ function apply(ctx, config) {
|
|
|
228
263
|
body { margin: 0; padding: 0; font-family: ${fontStack}; background: transparent; }
|
|
229
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; }
|
|
230
265
|
.cover { width: 220px; min-height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
|
|
231
|
-
|
|
232
|
-
/* ID Badge 分体式:使用 flex 强制垂直居中,消除字体基线差异 */
|
|
233
266
|
.id-badge-container {
|
|
234
267
|
position: absolute; top: 15px; left: 15px;
|
|
235
268
|
display: flex;
|
|
@@ -237,7 +270,7 @@ function apply(ctx, config) {
|
|
|
237
270
|
border-radius: 6px;
|
|
238
271
|
overflow: hidden;
|
|
239
272
|
border: 1px solid rgba(255,255,255,0.3);
|
|
240
|
-
height: 28px;
|
|
273
|
+
height: 28px;
|
|
241
274
|
}
|
|
242
275
|
.id-label {
|
|
243
276
|
background: #EE6E73;
|
|
@@ -249,7 +282,7 @@ function apply(ctx, config) {
|
|
|
249
282
|
text-transform: uppercase;
|
|
250
283
|
display: flex; align-items: center; justify-content: center;
|
|
251
284
|
height: 100%;
|
|
252
|
-
line-height: 1; margin: 0;
|
|
285
|
+
line-height: 1; margin: 0;
|
|
253
286
|
}
|
|
254
287
|
.id-val {
|
|
255
288
|
background: #fff;
|
|
@@ -260,23 +293,17 @@ function apply(ctx, config) {
|
|
|
260
293
|
font-weight: 900;
|
|
261
294
|
display: flex; align-items: center; justify-content: center;
|
|
262
295
|
height: 100%;
|
|
263
|
-
line-height: 1; margin: 0;
|
|
296
|
+
line-height: 1; margin: 0;
|
|
264
297
|
}
|
|
265
|
-
|
|
266
298
|
.info { flex: 1; padding: 26px; display: flex; flex-direction: column; overflow: hidden; position: relative; }
|
|
267
|
-
|
|
268
299
|
.header-group { flex-shrink: 0; margin-bottom: 16px; border-bottom: 2px solid #f5f5f5; padding-bottom: 12px; }
|
|
269
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; }
|
|
270
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; }
|
|
271
|
-
|
|
272
302
|
.author { font-size: 14px; color: #78909C; margin-top: 12px; font-weight: 400; display:flex; align-items:center; }
|
|
273
|
-
|
|
274
303
|
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 14px; flex-shrink: 0; }
|
|
275
304
|
.tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
|
|
276
|
-
|
|
277
305
|
.summary-box { flex: 1; position: relative; overflow: hidden; min-height: 0; margin-bottom: 16px; }
|
|
278
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; }
|
|
279
|
-
|
|
280
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; }
|
|
281
308
|
.stat b { color: #455a64; font-weight: bold; margin-right: 3px;}
|
|
282
309
|
</style></head><body>
|
|
@@ -295,18 +322,24 @@ function apply(ctx, config) {
|
|
|
295
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>
|
|
296
323
|
<span class="stat"><b style="color:#4caf50">赞</b>${likes}</span><span class="stat"><b style="color:#8d6e63">字数</b>${info.WordCount || 0}</span>
|
|
297
324
|
</div></div></div></body></html>`;
|
|
325
|
+
debugLog(`Card HTML prepared (approx ${html.length} chars). Launching Puppeteer...`);
|
|
298
326
|
const page = await ctx.puppeteer.page();
|
|
299
327
|
try {
|
|
300
328
|
await injectCookies(page);
|
|
301
329
|
await page.setContent(html);
|
|
302
330
|
await page.setViewport({ width: 660, height: 480, deviceScaleFactor: 3 });
|
|
303
331
|
const img = await page.$(".card").then((e) => e.screenshot({ type: "jpeg", quality: 100 }));
|
|
332
|
+
debugLog("Card screenshot captured successfully.");
|
|
304
333
|
return img;
|
|
334
|
+
} catch (e) {
|
|
335
|
+
logger.error("Error rendering card:", e);
|
|
336
|
+
throw e;
|
|
305
337
|
} finally {
|
|
306
338
|
await page.close();
|
|
307
339
|
}
|
|
308
340
|
}, "renderCard");
|
|
309
341
|
const renderSearchResults = /* @__PURE__ */ __name(async (keyword, results) => {
|
|
342
|
+
debugLog(`Rendering search results: ${results.length} items`);
|
|
310
343
|
const html = `<!DOCTYPE html><html><head><style>
|
|
311
344
|
body { margin: 0; padding: 0; font-family: ${fontStack}; width: 500px; background: transparent; }
|
|
312
345
|
.container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 10px; }
|
|
@@ -319,8 +352,6 @@ function apply(ctx, config) {
|
|
|
319
352
|
.content { flex: 1; display: flex; flex-direction: column; justify-content: space-between; height: 100%; min-width: 0; }
|
|
320
353
|
.top-row { display: flex; justify-content: space-between; align-items: flex-start; }
|
|
321
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;}
|
|
322
|
-
|
|
323
|
-
/* ID Badge Search: 迷你分体式,保持与大图风格一致但更紧凑 */
|
|
324
355
|
.id-badge {
|
|
325
356
|
display: flex;
|
|
326
357
|
border-radius: 4px;
|
|
@@ -338,7 +369,7 @@ function apply(ctx, config) {
|
|
|
338
369
|
font-weight: bold;
|
|
339
370
|
display: flex; align-items: center; justify-content: center;
|
|
340
371
|
height: 100%;
|
|
341
|
-
line-height: 1; margin: 0;
|
|
372
|
+
line-height: 1; margin: 0;
|
|
342
373
|
}
|
|
343
374
|
.id-val {
|
|
344
375
|
background: #fff;
|
|
@@ -349,9 +380,8 @@ function apply(ctx, config) {
|
|
|
349
380
|
font-weight: bold;
|
|
350
381
|
display: flex; align-items: center; justify-content: center;
|
|
351
382
|
height: 100%;
|
|
352
|
-
line-height: 1; margin: 0;
|
|
383
|
+
line-height: 1; margin: 0;
|
|
353
384
|
}
|
|
354
|
-
|
|
355
385
|
.author { font-size: 12px; color: #78909C; }
|
|
356
386
|
.tags { display: flex; gap: 4px; flex-wrap: wrap; height: 18px; overflow: hidden; margin-top: 4px; }
|
|
357
387
|
.tag { background: #f3f3f3; color: #666; padding: 0 5px; border-radius: 3px; font-size: 10px; white-space: nowrap; line-height: 1.6;}
|
|
@@ -378,12 +408,14 @@ function apply(ctx, config) {
|
|
|
378
408
|
await page.setContent(html);
|
|
379
409
|
await page.setViewport({ width: 550, height: 800, deviceScaleFactor: 3 });
|
|
380
410
|
const img = await page.$(".container").then((e) => e.screenshot({ type: "jpeg", quality: 100 }));
|
|
411
|
+
debugLog("Search results screenshot taken.");
|
|
381
412
|
return img;
|
|
382
413
|
} finally {
|
|
383
414
|
await page.close();
|
|
384
415
|
}
|
|
385
416
|
}, "renderSearchResults");
|
|
386
417
|
const renderReadPages = /* @__PURE__ */ __name(async (info) => {
|
|
418
|
+
debugLog(`Rendering read pages for ${info.ID} (${info.Title})`);
|
|
387
419
|
const content = cleanContent(info.Content);
|
|
388
420
|
const headerHeight = 40;
|
|
389
421
|
const footerHeight = 30;
|
|
@@ -399,10 +431,8 @@ function apply(ctx, config) {
|
|
|
399
431
|
const marginTop = Math.floor((maxContentHeight - optimalContentHeight) / 2) + headerHeight;
|
|
400
432
|
const html = `<!DOCTYPE html><html><head><style>
|
|
401
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;}
|
|
402
|
-
/* Header Padding reduced to 12px to align closer to left edge */
|
|
403
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; }
|
|
404
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; }
|
|
405
|
-
/* 修复1: 强制重置 header 和 footer 的缩进,防止 p, div 全局规则影响 */
|
|
406
436
|
.fixed-header, .fixed-footer, .header-title, .header-author { text-indent: 0 !important; }
|
|
407
437
|
|
|
408
438
|
#viewport { position: absolute; top: ${marginTop}px; left: ${paddingX}px; width: ${contentWidth}px; height: ${optimalContentHeight}px; overflow: hidden; }
|
|
@@ -412,11 +442,8 @@ function apply(ctx, config) {
|
|
|
412
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; }
|
|
413
443
|
.align-right { text-align: right !important; text-indent: 0 !important; margin-top: 0.5em; color: #666; font-style: italic; }
|
|
414
444
|
.no-indent { text-indent: 0 !important; }
|
|
415
|
-
|
|
416
|
-
/* Header Title absolute left */
|
|
417
445
|
.header-title { flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; text-align: left; min-width: 0; margin-right: 10px; }
|
|
418
446
|
.header-author { flex-shrink: 0; color: #78909C; max-width: 35%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; text-align: right; }
|
|
419
|
-
|
|
420
447
|
blockquote { margin: 1em 0.5em; padding-left: 1em; border-left: 4px solid #EE6E73; color: #666; }
|
|
421
448
|
blockquote p { text-indent: 0; margin: 0.3em 0; }
|
|
422
449
|
ul, ol { margin: 0.5em 0; padding-left: 1.5em; }
|
|
@@ -451,6 +478,7 @@ function apply(ctx, config) {
|
|
|
451
478
|
await injectCookies(page);
|
|
452
479
|
await page.setContent(html);
|
|
453
480
|
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 3 });
|
|
481
|
+
debugLog("Waiting for fonts and images...");
|
|
454
482
|
await page.evaluate(async () => {
|
|
455
483
|
await document.fonts.ready;
|
|
456
484
|
await new Promise((resolve) => {
|
|
@@ -475,6 +503,7 @@ function apply(ctx, config) {
|
|
|
475
503
|
const step = contentWidth + columnGap;
|
|
476
504
|
const totalPages = Math.floor((scrollWidth + columnGap - 10) / step) + 1;
|
|
477
505
|
const finalPages = Math.max(1, totalPages);
|
|
506
|
+
debugLog(`Content width calculated: ${scrollWidth}px. Splitting into ${finalPages} pages.`);
|
|
478
507
|
const imgs = [];
|
|
479
508
|
for (let i = 0; i < finalPages; i++) {
|
|
480
509
|
await page.evaluate((idx, stepPx, curr, total) => {
|
|
@@ -484,18 +513,24 @@ function apply(ctx, config) {
|
|
|
484
513
|
}, i, step, i + 1, finalPages);
|
|
485
514
|
imgs.push(await page.screenshot({ type: "jpeg", quality: 100 }));
|
|
486
515
|
}
|
|
516
|
+
debugLog("All pages captured.");
|
|
487
517
|
return imgs;
|
|
518
|
+
} catch (e) {
|
|
519
|
+
logger.error("Error rendering read pages:", e);
|
|
520
|
+
throw e;
|
|
488
521
|
} finally {
|
|
489
522
|
await page.close();
|
|
490
523
|
}
|
|
491
524
|
}, "renderReadPages");
|
|
492
525
|
ctx.command("ft.info <threadId:string>", "预览作品").action(async ({ session }, threadId) => {
|
|
526
|
+
debugLog(`CMD ft.info triggered by ${session.userId} for ID: ${threadId}`);
|
|
493
527
|
if (!threadId) return "请输入ID";
|
|
494
528
|
const res = await fetchThread(threadId);
|
|
495
529
|
if (!res.valid) return `[错误] ${res.msg}`;
|
|
496
530
|
return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
|
|
497
531
|
});
|
|
498
532
|
ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
|
|
533
|
+
debugLog(`CMD ft.read triggered by ${session.userId} for ID: ${threadId}`);
|
|
499
534
|
if (!threadId) return "请输入ID";
|
|
500
535
|
const res = await fetchThread(threadId);
|
|
501
536
|
if (!res.valid) return `[错误] 读取失败: ${res.msg}`;
|
|
@@ -516,11 +551,12 @@ function apply(ctx, config) {
|
|
|
516
551
|
if (navs.length) nodes.push((0, import_koishi.h)("message", import_koishi.h.text("章节导航:\n" + navs.join("\n"))));
|
|
517
552
|
return session.send((0, import_koishi.h)("message", { forward: true }, nodes));
|
|
518
553
|
} catch (e) {
|
|
519
|
-
|
|
554
|
+
logger.error("ft.read rendering failed:", e);
|
|
520
555
|
return "[错误] 渲染失败";
|
|
521
556
|
}
|
|
522
557
|
});
|
|
523
558
|
ctx.command("ft.random", "随机作品").action(async ({ session }) => {
|
|
559
|
+
debugLog(`CMD ft.random triggered by ${session.userId}`);
|
|
524
560
|
const id = await fetchRandomId();
|
|
525
561
|
if (!id) return "[错误] 获取失败";
|
|
526
562
|
const res = await fetchThread(id);
|
|
@@ -529,6 +565,7 @@ function apply(ctx, config) {
|
|
|
529
565
|
return `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
|
|
530
566
|
});
|
|
531
567
|
ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
|
|
568
|
+
debugLog(`CMD ft.search triggered by ${session.userId} for "${keyword}"`);
|
|
532
569
|
if (!keyword) return "请输入关键词";
|
|
533
570
|
await session.send("[加载中] 搜索中...");
|
|
534
571
|
const results = await searchThreads(keyword);
|
|
@@ -538,6 +575,7 @@ function apply(ctx, config) {
|
|
|
538
575
|
return `Tip: 发送 /ft.read [ID] 阅读 (例: /ft.read ${exampleId})`;
|
|
539
576
|
});
|
|
540
577
|
ctx.command("ft.sub <threadId:string>", "订阅").action(async ({ session }, threadId) => {
|
|
578
|
+
debugLog(`CMD ft.sub triggered by ${session.userId} for ID: ${threadId}`);
|
|
541
579
|
if (!/^\d+$/.test(threadId)) return "ID错误";
|
|
542
580
|
const exist = await ctx.database.get("fimtale_subs", { cid: session.cid, threadId });
|
|
543
581
|
if (exist.length) return "已订阅";
|
|
@@ -548,6 +586,7 @@ function apply(ctx, config) {
|
|
|
548
586
|
return session.send(import_koishi.h.image(await renderCard(res.data, res.parent), "image/jpeg"));
|
|
549
587
|
});
|
|
550
588
|
ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
|
|
589
|
+
debugLog(`CMD ft.unsub triggered by ${session.userId} for ID: ${threadId}`);
|
|
551
590
|
const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
|
|
552
591
|
return res.matched ? "[成功] 已退订" : "未找到订阅";
|
|
553
592
|
});
|
|
@@ -555,6 +594,7 @@ function apply(ctx, config) {
|
|
|
555
594
|
if (!config.autoParseLink) return next();
|
|
556
595
|
const matches = [...session.content.matchAll(/fimtale\.(?:com|net)\/t\/(\d+)/g)];
|
|
557
596
|
if (matches.length === 0) return next();
|
|
597
|
+
debugLog(`Middleware matched link in channel ${session.channelId}`);
|
|
558
598
|
const uniqueIds = [...new Set(matches.map((m) => m[1]))];
|
|
559
599
|
if (session.userId === session.selfId) return next();
|
|
560
600
|
const messageNodes = [];
|
|
@@ -566,6 +606,7 @@ function apply(ctx, config) {
|
|
|
566
606
|
messageNodes.push((0, import_koishi.h)("message", import_koishi.h.image(img, "image/jpeg")));
|
|
567
607
|
}
|
|
568
608
|
} catch (e) {
|
|
609
|
+
logger.error(`Middleware failed to process link ${id}:`, e);
|
|
569
610
|
}
|
|
570
611
|
}
|
|
571
612
|
if (messageNodes.length === 0) return next();
|
|
@@ -578,12 +619,14 @@ function apply(ctx, config) {
|
|
|
578
619
|
ctx.setInterval(async () => {
|
|
579
620
|
const subs = await ctx.database.get("fimtale_subs", {});
|
|
580
621
|
if (!subs.length) return;
|
|
622
|
+
debugLog(`Polling check started for ${subs.length} subscriptions.`);
|
|
581
623
|
const tids = [...new Set(subs.map((s) => s.threadId))];
|
|
582
624
|
for (const tid of tids) {
|
|
583
625
|
const res = await fetchThread(tid);
|
|
584
626
|
if (!res.valid) continue;
|
|
585
627
|
const targets = subs.filter((s) => s.threadId === tid && s.lastCount < res.data.Comments);
|
|
586
628
|
if (targets.length) {
|
|
629
|
+
debugLog(`Update found for thread ${tid} (Old: ${targets[0].lastCount}, New: ${res.data.Comments})`);
|
|
587
630
|
const msg = `[更新] ${res.data.Title} 更新了!
|
|
588
631
|
回复: ${res.data.Comments}
|
|
589
632
|
https://fimtale.com/t/${tid}`;
|
|
@@ -591,7 +634,8 @@ https://fimtale.com/t/${tid}`;
|
|
|
591
634
|
try {
|
|
592
635
|
await ctx.broadcast([sub.cid], import_koishi.h.parse(msg));
|
|
593
636
|
await ctx.database.set("fimtale_subs", { id: sub.id }, { lastCount: res.data.Comments });
|
|
594
|
-
} catch {
|
|
637
|
+
} catch (e) {
|
|
638
|
+
logger.error(`Broadcast failed for sub ${sub.id}:`, e);
|
|
595
639
|
}
|
|
596
640
|
}
|
|
597
641
|
}
|