koishi-plugin-fimtale-api 0.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.d.ts ADDED
@@ -0,0 +1,36 @@
1
+ import { Context, Schema } from 'koishi';
2
+ export declare const name = "fimtale-watcher";
3
+ export declare const inject: string[];
4
+ declare module 'koishi' {
5
+ interface Tables {
6
+ fimtale_subs: FimtaleSub;
7
+ }
8
+ }
9
+ export interface FimtaleSub {
10
+ id: number;
11
+ cid: string;
12
+ threadId: string;
13
+ lastCount: number;
14
+ lastCheck: number;
15
+ }
16
+ export interface Config {
17
+ apiUrl: string;
18
+ apiKey: string;
19
+ apiPass: string;
20
+ pollInterval: number;
21
+ autoParseLink: boolean;
22
+ showPreview: boolean;
23
+ previewLength: number;
24
+ readTheme: 'light' | 'dark' | 'sepia';
25
+ messages: {
26
+ subSuccess: string;
27
+ subExists: string;
28
+ unsubSuccess: string;
29
+ notFound: string;
30
+ updateAlert: string;
31
+ infoTemplate: string;
32
+ errorApi: string;
33
+ };
34
+ }
35
+ export declare const Config: Schema<Config>;
36
+ export declare function apply(ctx: Context, config: Config): void;
package/lib/index.js ADDED
@@ -0,0 +1,299 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
6
+ var __export = (target, all) => {
7
+ for (var name2 in all)
8
+ __defProp(target, name2, { get: all[name2], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
+ Config: () => Config,
24
+ apply: () => apply,
25
+ inject: () => inject,
26
+ name: () => name
27
+ });
28
+ module.exports = __toCommonJS(src_exports);
29
+ var import_koishi = require("koishi");
30
+ var name = "fimtale-watcher";
31
+ var inject = ["puppeteer", "database", "http"];
32
+ var Config = import_koishi.Schema.object({
33
+ apiUrl: import_koishi.Schema.string().default("https://fimtale.com/api/v1").description("Fimtale API 基础路径"),
34
+ apiKey: import_koishi.Schema.string().role("secret").required().description("API Key (必填)"),
35
+ apiPass: import_koishi.Schema.string().role("secret").required().description("API Pass (必填)"),
36
+ pollInterval: import_koishi.Schema.number().default(60 * 1e3).description("追更轮询间隔(毫秒)"),
37
+ autoParseLink: import_koishi.Schema.boolean().default(true).description("是否开启链接自动解析"),
38
+ showPreview: import_koishi.Schema.boolean().default(true).description("推送或查询时是否显示文字预览"),
39
+ previewLength: import_koishi.Schema.number().default(100).description("预览文本的字数限制"),
40
+ readTheme: import_koishi.Schema.union([
41
+ import_koishi.Schema.const("light").description("明亮"),
42
+ import_koishi.Schema.const("dark").description("暗黑"),
43
+ import_koishi.Schema.const("sepia").description("羊皮纸(护眼)")
44
+ ]).default("sepia").description("阅读器图片生成的背景主题"),
45
+ messages: import_koishi.Schema.object({
46
+ subSuccess: import_koishi.Schema.string().default("✅ 订阅成功!\n📖 标题:{title}\n✍️ 作者:{author}\n当前 {count} 个回复"),
47
+ subExists: import_koishi.Schema.string().default("⚠️ 你已经订阅过这个帖子了。"),
48
+ unsubSuccess: import_koishi.Schema.string().default("❎ 已取消订阅帖子:{id}"),
49
+ notFound: import_koishi.Schema.string().default("❌ 帖子不存在或 API 请求失败。"),
50
+ updateAlert: import_koishi.Schema.string().default("📢 <b>{title}</b> 有新动态!\n\n最新回复:{lastUser}\n回复总数:{count}\n\n传送门:{url}"),
51
+ infoTemplate: import_koishi.Schema.string().default("📄 <b>{title}</b>\n作者:{author}\n热度:{views} | 回复:{count}\n最后更新:{time}\n\n{preview}"),
52
+ errorApi: import_koishi.Schema.string().default("❌ API 连接失败,请检查配置。")
53
+ }).description("自定义提示文案 (支持 {变量} 替换)")
54
+ });
55
+ function apply(ctx, config) {
56
+ ctx.model.extend("fimtale_subs", {
57
+ id: "unsigned",
58
+ cid: "string",
59
+ threadId: "string",
60
+ lastCount: "integer",
61
+ lastCheck: "integer"
62
+ }, {
63
+ primary: "id",
64
+ autoInc: true
65
+ });
66
+ const stripHtml = /* @__PURE__ */ __name((html) => {
67
+ if (!html) return "";
68
+ return html.replace(/<[^>]+>/g, "").replace(/&nbsp;/g, " ").replace(/\s+/g, " ").trim();
69
+ }, "stripHtml");
70
+ const formatTime = /* @__PURE__ */ __name((timestamp) => {
71
+ if (!timestamp) return "未知";
72
+ return import_koishi.Time.format(timestamp * 1e3);
73
+ }, "formatTime");
74
+ const formatText = /* @__PURE__ */ __name((template, data) => {
75
+ return template.replace(/\{(\w+)\}/g, (_, key) => data[key] ?? "");
76
+ }, "formatText");
77
+ const fetchThread = /* @__PURE__ */ __name(async (threadId) => {
78
+ try {
79
+ const endpoint = `${config.apiUrl}/t/${threadId}`;
80
+ const params = { APIKey: config.apiKey, APIPass: config.apiPass };
81
+ const res = await ctx.http.get(endpoint, { params });
82
+ if (!res || res.Status !== 1 || !res.TopicInfo) {
83
+ return { valid: false };
84
+ }
85
+ const info = res.TopicInfo;
86
+ return {
87
+ valid: true,
88
+ info,
89
+ // 原始完整信息
90
+ menu: res.Menu || [],
91
+ // 目录数组
92
+ // 提取常用字段方便使用
93
+ id: info.ID,
94
+ title: info.Title,
95
+ author: info.UserName,
96
+ views: info.Views,
97
+ comments: info.Comments,
98
+ lastTime: formatTime(info.LastTime),
99
+ lastUser: info.LastUser,
100
+ content: info.Content,
101
+ dateCreated: info.DateCreated
102
+ };
103
+ } catch (e) {
104
+ ctx.logger("fimtale").warn(`Fetch error for ${threadId}: ${e}`);
105
+ return { valid: false };
106
+ }
107
+ }, "fetchThread");
108
+ ctx.command("ft.sub <threadId:string>", "订阅帖子追更").action(async ({ session }, threadId) => {
109
+ if (!threadId || !/^\d+$/.test(threadId)) return "请输入纯数字的帖子ID。";
110
+ const existing = await ctx.database.get("fimtale_subs", { cid: session.cid, threadId });
111
+ if (existing.length > 0) return config.messages.subExists;
112
+ const data = await fetchThread(threadId);
113
+ if (!data.valid) return config.messages.notFound;
114
+ await ctx.database.create("fimtale_subs", {
115
+ cid: session.cid,
116
+ threadId,
117
+ lastCount: data.comments,
118
+ lastCheck: Date.now()
119
+ });
120
+ return formatText(config.messages.subSuccess, {
121
+ id: threadId,
122
+ title: data.title,
123
+ author: data.author,
124
+ count: data.comments
125
+ });
126
+ });
127
+ ctx.command("ft.unsub <threadId:string>", "取消订阅").action(async ({ session }, threadId) => {
128
+ if (!threadId) return "请输入帖子ID。";
129
+ const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
130
+ return res.matched ? formatText(config.messages.unsubSuccess, { id: threadId }) : "未找到对应订阅。";
131
+ });
132
+ ctx.command("ft.info <threadId:string>", "查询帖子详情").action(async (_, threadId) => {
133
+ if (!threadId) return "请输入ID。";
134
+ const data = await fetchThread(threadId);
135
+ if (!data.valid) return config.messages.notFound;
136
+ let preview = "";
137
+ if (config.showPreview) {
138
+ const rawText = stripHtml(data.content);
139
+ preview = rawText.length > config.previewLength ? rawText.substring(0, config.previewLength) + "..." : rawText;
140
+ }
141
+ return formatText(config.messages.infoTemplate, {
142
+ title: data.title,
143
+ author: data.author,
144
+ views: data.views,
145
+ count: data.comments,
146
+ time: data.lastTime,
147
+ preview
148
+ });
149
+ });
150
+ ctx.command("ft.read <threadId:string>", "阅读章节(生成长图)").action(async ({ session }, threadId) => {
151
+ if (!threadId) return "请输入章节ID。";
152
+ const data = await fetchThread(threadId);
153
+ if (!data.valid) return "❌ 读取失败,可能是网络问题或帖子不存在。";
154
+ const info = data.info;
155
+ const menu = data.menu;
156
+ let prevId = null;
157
+ let nextId = null;
158
+ if (menu && menu.length > 0) {
159
+ const currentIndex = menu.findIndex((item) => item.ID.toString() === threadId);
160
+ if (currentIndex !== -1) {
161
+ if (currentIndex > 0) prevId = menu[currentIndex - 1].ID;
162
+ if (currentIndex < menu.length - 1) nextId = menu[currentIndex + 1].ID;
163
+ }
164
+ }
165
+ const themes = {
166
+ light: { bg: "#ffffff", text: "#333333", accent: "#666" },
167
+ dark: { bg: "#2b2b2b", text: "#dcdcdc", accent: "#888" },
168
+ sepia: { bg: "#f5e8d0", text: "#5b4636", accent: "#8b7355" }
169
+ // 羊皮纸
170
+ };
171
+ const t = themes[config.readTheme];
172
+ const htmlContent = `
173
+ <!DOCTYPE html>
174
+ <html>
175
+ <head>
176
+ <meta charset="UTF-8">
177
+ <style>
178
+ body {
179
+ font-family: "Microsoft YaHei", "PingFang SC", sans-serif;
180
+ background-color: ${t.bg};
181
+ color: ${t.text};
182
+ padding: 40px;
183
+ width: 500px; /* 限制宽度适合手机观看 */
184
+ line-height: 1.8;
185
+ word-wrap: break-word;
186
+ }
187
+ .header { margin-bottom: 25px; border-bottom: 2px solid ${t.accent}; padding-bottom: 15px; }
188
+ .title { font-size: 26px; font-weight: bold; margin-bottom: 8px; }
189
+ .meta { font-size: 14px; opacity: 0.8; }
190
+ .content { font-size: 18px; text-align: justify; }
191
+ /* Fimtale 的内容通常包含 p 标签,我们做一些优化 */
192
+ .content p { margin-bottom: 1.2em; text-indent: 2em; }
193
+ .content img { max-width: 100%; height: auto; border-radius: 6px; display: block; margin: 10px auto; }
194
+ .footer { margin-top: 40px; text-align: center; font-size: 12px; opacity: 0.6; }
195
+ </style>
196
+ </head>
197
+ <body>
198
+ <div class="header">
199
+ <div class="title">${info.Title}</div>
200
+ <div class="meta">
201
+ 作者:${info.UserName} &nbsp;|&nbsp; ${import_koishi.Time.format(info.DateCreated * 1e3).split(" ")[0]}
202
+ </div>
203
+ </div>
204
+ <div class="content">
205
+ ${info.Content}
206
+ </div>
207
+ <div class="footer">
208
+ FimTale Reader | ID: ${info.ID}
209
+ </div>
210
+ </body>
211
+ </html>`;
212
+ let imgBuf;
213
+ try {
214
+ const page = await ctx.puppeteer.page();
215
+ await page.setContent(htmlContent);
216
+ await new Promise((r) => setTimeout(r, 500));
217
+ const body = await page.$("body");
218
+ imgBuf = await body.screenshot({ type: "png" });
219
+ await page.close();
220
+ } catch (e) {
221
+ ctx.logger("fimtale").error(e);
222
+ return "❌ 生成图片失败,请检查服务器 Puppeteer 配置。";
223
+ }
224
+ const messageElements = [
225
+ import_koishi.h.image(imgBuf, "image/png")
226
+ ];
227
+ const navs = [];
228
+ if (prevId) navs.push(`⬅️上一章: ft.read ${prevId}`);
229
+ if (nextId) navs.push(`➡️下一章: ft.read ${nextId}`);
230
+ if (!nextId && (!menu.length || menu.length === 0)) {
231
+ navs.push(`(未检测到目录,尝试下页: ft.read ${parseInt(threadId) + 1})`);
232
+ }
233
+ if (navs.length > 0) {
234
+ messageElements.push(import_koishi.h.text("\n" + navs.join("\n")));
235
+ }
236
+ return messageElements;
237
+ });
238
+ ctx.setInterval(async () => {
239
+ const subs = await ctx.database.get("fimtale_subs", {});
240
+ if (!subs.length) return;
241
+ const threadIds = [...new Set(subs.map((s) => s.threadId))];
242
+ for (const tid of threadIds) {
243
+ const data = await fetchThread(tid);
244
+ if (!data.valid) continue;
245
+ const targets = subs.filter((s) => s.threadId === tid && s.lastCount < data.comments);
246
+ if (targets.length > 0) {
247
+ const msg = formatText(config.messages.updateAlert, {
248
+ title: data.title,
249
+ lastUser: data.lastUser || "神秘小马",
250
+ count: data.comments,
251
+ url: `https://fimtale.com/t/${tid}`
252
+ });
253
+ for (const sub of targets) {
254
+ try {
255
+ await ctx.broadcast([sub.cid], msg);
256
+ await ctx.database.set("fimtale_subs", { id: sub.id }, {
257
+ lastCount: data.comments,
258
+ lastCheck: Date.now()
259
+ });
260
+ } catch (e) {
261
+ }
262
+ }
263
+ }
264
+ }
265
+ }, config.pollInterval);
266
+ ctx.middleware(async (session, next) => {
267
+ if (!config.autoParseLink) return next();
268
+ const match = session.content.match(/fimtale\.com\/t\/(\d+)/);
269
+ if (match && match[1]) {
270
+ if (session.userId === session.selfId) return next();
271
+ const threadId = match[1];
272
+ const data = await fetchThread(threadId);
273
+ if (data.valid) {
274
+ let preview = "";
275
+ if (config.showPreview) {
276
+ const rawText = stripHtml(data.content);
277
+ preview = rawText.length > config.previewLength ? rawText.substring(0, config.previewLength) + "..." : rawText;
278
+ }
279
+ return session.send(formatText(config.messages.infoTemplate, {
280
+ title: data.title,
281
+ author: data.author,
282
+ views: data.views,
283
+ count: data.comments,
284
+ time: data.lastTime,
285
+ preview
286
+ }));
287
+ }
288
+ }
289
+ return next();
290
+ });
291
+ }
292
+ __name(apply, "apply");
293
+ // Annotate the CommonJS export names for ESM import in node:
294
+ 0 && (module.exports = {
295
+ Config,
296
+ apply,
297
+ inject,
298
+ name
299
+ });
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "koishi-plugin-fimtale-api",
3
+ "description": "自用Koishi插件,从fimtale-api获取信息",
4
+ "version": "0.0.1",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "dist"
10
+ ],
11
+ "license": "MIT",
12
+ "keywords": [
13
+ "chatbot",
14
+ "koishi",
15
+ "plugin"
16
+ ],
17
+ "peerDependencies": {
18
+ "koishi": "^4.18.7"
19
+ }
20
+ }
package/readme.md ADDED
@@ -0,0 +1,5 @@
1
+ # koishi-plugin-fimtale-api
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-fimtale-api?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-fimtale-api)
4
+
5
+ Koishi插件,从fimtale-api获取信息