koishi-plugin-fimtale-api 0.0.1 → 0.0.3
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 +4 -12
- package/lib/index.js +446 -211
- package/package.json +5 -3
- package/readme.md +3 -1
package/lib/index.d.ts
CHANGED
|
@@ -17,20 +17,12 @@ export interface Config {
|
|
|
17
17
|
apiUrl: string;
|
|
18
18
|
apiKey: string;
|
|
19
19
|
apiPass: string;
|
|
20
|
+
cookies: string;
|
|
20
21
|
pollInterval: number;
|
|
21
22
|
autoParseLink: boolean;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
messages: {
|
|
26
|
-
subSuccess: string;
|
|
27
|
-
subExists: string;
|
|
28
|
-
unsubSuccess: string;
|
|
29
|
-
notFound: string;
|
|
30
|
-
updateAlert: string;
|
|
31
|
-
infoTemplate: string;
|
|
32
|
-
errorApi: string;
|
|
33
|
-
};
|
|
23
|
+
deviceWidth: number;
|
|
24
|
+
deviceHeight: number;
|
|
25
|
+
fontSize: number;
|
|
34
26
|
}
|
|
35
27
|
export declare const Config: Schema<Config>;
|
|
36
28
|
export declare function apply(ctx: Context, config: Config): void;
|
package/lib/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
1
2
|
var __defProp = Object.defineProperty;
|
|
2
3
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
3
4
|
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
4
6
|
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
5
7
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
6
8
|
var __export = (target, all) => {
|
|
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
15
17
|
}
|
|
16
18
|
return to;
|
|
17
19
|
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
18
28
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
29
|
|
|
20
30
|
// src/index.ts
|
|
@@ -27,30 +37,19 @@ __export(src_exports, {
|
|
|
27
37
|
});
|
|
28
38
|
module.exports = __toCommonJS(src_exports);
|
|
29
39
|
var import_koishi = require("koishi");
|
|
40
|
+
var import_crypto = __toESM(require("crypto"));
|
|
30
41
|
var name = "fimtale-watcher";
|
|
31
42
|
var inject = ["puppeteer", "database", "http"];
|
|
32
43
|
var Config = import_koishi.Schema.object({
|
|
33
44
|
apiUrl: import_koishi.Schema.string().default("https://fimtale.com/api/v1").description("Fimtale API 基础路径"),
|
|
34
45
|
apiKey: import_koishi.Schema.string().role("secret").required().description("API Key (必填)"),
|
|
35
46
|
apiPass: import_koishi.Schema.string().role("secret").required().description("API Pass (必填)"),
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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("自定义提示文案 (支持 {变量} 替换)")
|
|
47
|
+
cookies: import_koishi.Schema.string().role("secret").description("浏览器 Cookie (用于解除安全模式,必填)"),
|
|
48
|
+
pollInterval: import_koishi.Schema.number().default(60 * 1e3).description("追更轮询间隔(ms)"),
|
|
49
|
+
autoParseLink: import_koishi.Schema.boolean().default(true).description("自动解析链接为预览卡片"),
|
|
50
|
+
deviceWidth: import_koishi.Schema.number().default(390).description("阅读器渲染宽度(px)"),
|
|
51
|
+
deviceHeight: import_koishi.Schema.number().default(844).description("阅读器渲染高度(px)"),
|
|
52
|
+
fontSize: import_koishi.Schema.number().default(22).description("正文字号(px)")
|
|
54
53
|
});
|
|
55
54
|
function apply(ctx, config) {
|
|
56
55
|
ctx.model.extend("fimtale_subs", {
|
|
@@ -59,235 +58,471 @@ function apply(ctx, config) {
|
|
|
59
58
|
threadId: "string",
|
|
60
59
|
lastCount: "integer",
|
|
61
60
|
lastCheck: "integer"
|
|
62
|
-
}, {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
61
|
+
}, { primary: "id", autoInc: true });
|
|
62
|
+
const sleep = /* @__PURE__ */ __name((ms) => new Promise((resolve) => setTimeout(resolve, ms)), "sleep");
|
|
63
|
+
const formatDate = /* @__PURE__ */ __name((timestamp) => {
|
|
64
|
+
if (!timestamp) return "Unknown";
|
|
65
|
+
const date = new Date(timestamp * 1e3);
|
|
66
|
+
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, "0")}-${date.getDate().toString().padStart(2, "0")}`;
|
|
67
|
+
}, "formatDate");
|
|
66
68
|
const stripHtml = /* @__PURE__ */ __name((html) => {
|
|
67
69
|
if (!html) return "";
|
|
68
70
|
return html.replace(/<[^>]+>/g, "").replace(/ /g, " ").replace(/\s+/g, " ").trim();
|
|
69
71
|
}, "stripHtml");
|
|
70
|
-
const
|
|
71
|
-
if (!
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
const extractImage = /* @__PURE__ */ __name((html) => {
|
|
73
|
+
if (!html) return null;
|
|
74
|
+
const match = html.match(/<img[^>]+src="([^">]+)"/);
|
|
75
|
+
return match ? match[1] : null;
|
|
76
|
+
}, "extractImage");
|
|
77
|
+
const generateGradient = /* @__PURE__ */ __name((str) => {
|
|
78
|
+
const hash = import_crypto.default.createHash("md5").update(str || "default").digest("hex");
|
|
79
|
+
const c1 = "#" + hash.substring(0, 6);
|
|
80
|
+
const c2 = "#" + hash.substring(6, 12);
|
|
81
|
+
return `linear-gradient(135deg, ${c1} 0%, ${c2} 100%)`;
|
|
82
|
+
}, "generateGradient");
|
|
83
|
+
const injectCookies = /* @__PURE__ */ __name(async (page) => {
|
|
84
|
+
if (!config.cookies) return;
|
|
85
|
+
const cookies = config.cookies.split(";").map((pair) => {
|
|
86
|
+
const parts = pair.trim().split("=");
|
|
87
|
+
if (parts.length < 2) return null;
|
|
88
|
+
return { name: parts[0].trim(), value: parts.slice(1).join("=").trim(), domain: "fimtale.com", path: "/" };
|
|
89
|
+
}).filter((c) => c !== null);
|
|
90
|
+
if (cookies.length) await page.setCookie(...cookies);
|
|
91
|
+
}, "injectCookies");
|
|
77
92
|
const fetchThread = /* @__PURE__ */ __name(async (threadId) => {
|
|
78
93
|
try {
|
|
79
|
-
const
|
|
94
|
+
const url = `${config.apiUrl}/t/${threadId}`;
|
|
80
95
|
const params = { APIKey: config.apiKey, APIPass: config.apiPass };
|
|
81
|
-
const res = await ctx.http.get(
|
|
82
|
-
if (
|
|
83
|
-
return { valid: false };
|
|
84
|
-
}
|
|
85
|
-
const info = res.TopicInfo;
|
|
96
|
+
const res = await ctx.http.get(url, { params });
|
|
97
|
+
if (res.Status !== 1 || !res.TopicInfo) return { valid: false, msg: res.ErrorMessage || "API Error" };
|
|
86
98
|
return {
|
|
87
99
|
valid: true,
|
|
88
|
-
|
|
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
|
|
100
|
+
data: res.TopicInfo,
|
|
101
|
+
parent: res.ParentInfo,
|
|
102
|
+
menu: res.Menu || []
|
|
102
103
|
};
|
|
103
104
|
} catch (e) {
|
|
104
|
-
|
|
105
|
-
return { valid: false };
|
|
105
|
+
return { valid: false, msg: "Request Failed" };
|
|
106
106
|
}
|
|
107
107
|
}, "fetchThread");
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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;
|
|
108
|
+
const fetchRandomId = /* @__PURE__ */ __name(async () => {
|
|
109
|
+
try {
|
|
110
|
+
const headers = config.cookies ? { Cookie: config.cookies } : {};
|
|
111
|
+
const html = await ctx.http.get("https://fimtale.com/rand", { responseType: "text", headers });
|
|
112
|
+
let match = html.match(/FimTale\.topic\.init\((\d+)/) || html.match(/data-clipboard-text=".*?\/t\/(\d+)"/);
|
|
113
|
+
return match ? match[1] : null;
|
|
114
|
+
} catch (e) {
|
|
115
|
+
return null;
|
|
140
116
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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;
|
|
117
|
+
}, "fetchRandomId");
|
|
118
|
+
const searchThreads = /* @__PURE__ */ __name(async (keyword) => {
|
|
119
|
+
let page;
|
|
120
|
+
try {
|
|
121
|
+
page = await ctx.puppeteer.page();
|
|
122
|
+
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");
|
|
123
|
+
await injectCookies(page);
|
|
124
|
+
const searchUrl = `https://fimtale.com/topics?q=${encodeURIComponent(keyword)}`;
|
|
125
|
+
await page.goto(searchUrl, { waitUntil: "networkidle2", timeout: 25e3 });
|
|
126
|
+
try {
|
|
127
|
+
await page.waitForSelector(".card", { timeout: 5e3 });
|
|
128
|
+
} catch {
|
|
163
129
|
}
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
130
|
+
const results = await page.evaluate(() => {
|
|
131
|
+
const items = [];
|
|
132
|
+
const cards = document.querySelectorAll(".card.topic-card");
|
|
133
|
+
cards.forEach((card) => {
|
|
134
|
+
if (items.length >= 6) return;
|
|
135
|
+
const link = card.querySelector('a[href^="/t/"]');
|
|
136
|
+
const href = link?.getAttribute("href");
|
|
137
|
+
const idMatch = href?.match(/^\/t\/(\d+)$/);
|
|
138
|
+
if (!idMatch) return;
|
|
139
|
+
const id = idMatch[1];
|
|
140
|
+
if (items.some((i) => i.id === id)) return;
|
|
141
|
+
let title = "";
|
|
142
|
+
const titleEl = card.querySelector(".card-title");
|
|
143
|
+
if (titleEl) title = titleEl.textContent?.trim() || "";
|
|
144
|
+
else title = link.textContent?.trim() || "";
|
|
145
|
+
let author = "Unknown";
|
|
146
|
+
const authorEl = card.querySelector('a[href^="/u/"] span.grey-text');
|
|
147
|
+
if (authorEl) author = authorEl.textContent?.trim() || "";
|
|
148
|
+
let cover = void 0;
|
|
149
|
+
const imgEl = card.querySelector(".card-image img");
|
|
150
|
+
if (imgEl) {
|
|
151
|
+
const src = imgEl.getAttribute("src");
|
|
152
|
+
if (src && (!src.includes("avatar") || src.includes("upload"))) cover = src;
|
|
186
153
|
}
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
154
|
+
const tags = [];
|
|
155
|
+
let status = "";
|
|
156
|
+
card.querySelectorAll(".main-tag-set div").forEach((b) => {
|
|
157
|
+
const t = b.textContent?.trim();
|
|
158
|
+
if (t) tags.push(t);
|
|
159
|
+
});
|
|
160
|
+
card.querySelectorAll(".chip").forEach((c) => {
|
|
161
|
+
const t = c.textContent?.trim();
|
|
162
|
+
if (!t) return;
|
|
163
|
+
if (["连载中", "已完结", "已弃坑"].includes(t)) status = t;
|
|
164
|
+
else if (!t.includes("展开其余")) tags.push(t);
|
|
165
|
+
});
|
|
166
|
+
const stats = { views: "0", comments: "0", likes: "0", words: "0" };
|
|
167
|
+
const actionDiv = card.querySelector(".card-action > div");
|
|
168
|
+
if (actionDiv) {
|
|
169
|
+
actionDiv.querySelectorAll("span[title]").forEach((s) => {
|
|
170
|
+
const t = s.getAttribute("title") || "";
|
|
171
|
+
const v = s.textContent?.trim().replace(/,/g, "") || "0";
|
|
172
|
+
if (t.includes("字")) stats.words = v;
|
|
173
|
+
if (t.includes("阅读")) stats.views = v;
|
|
174
|
+
if (t.includes("评论")) stats.comments = v;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
const greenText = card.querySelector(".left.green-text");
|
|
178
|
+
if (greenText) stats.likes = greenText.textContent?.replace(/[^0-9]/g, "") || "0";
|
|
179
|
+
let updateTime = "";
|
|
180
|
+
const timeSpan = card.querySelector('div[style*="margin: 3px 0;"] span.grey-text');
|
|
181
|
+
if (timeSpan) {
|
|
182
|
+
const txt = timeSpan.textContent || "";
|
|
183
|
+
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*日)/);
|
|
184
|
+
if (dateMatch) updateTime = dateMatch[1].replace(/\s/g, "");
|
|
185
|
+
}
|
|
186
|
+
items.push({ id, title, author, cover, tags: tags.slice(0, 8), status, stats, updateTime });
|
|
187
|
+
});
|
|
188
|
+
return items;
|
|
189
|
+
});
|
|
190
|
+
return results;
|
|
191
|
+
} catch (e) {
|
|
192
|
+
return [];
|
|
193
|
+
} finally {
|
|
194
|
+
if (page) await page.close();
|
|
195
|
+
}
|
|
196
|
+
}, "searchThreads");
|
|
197
|
+
const renderCard = /* @__PURE__ */ __name(async (info, parent) => {
|
|
198
|
+
const isChapter = info.IsChapter || !!parent && parent.ID !== info.ID;
|
|
199
|
+
const displayTitle = isChapter && parent ? parent.Title : info.Title;
|
|
200
|
+
const displayCover = isChapter && parent ? parent.Background || extractImage(parent.Content) : info.Background || extractImage(info.Content);
|
|
201
|
+
const displayTagsObj = isChapter && parent ? parent.Tags : info.Tags;
|
|
202
|
+
const subTitle = isChapter ? info.Title : null;
|
|
203
|
+
const views = info.Views || 0;
|
|
204
|
+
const comments = info.Comments || 0;
|
|
205
|
+
const words = info.WordCount || 0;
|
|
206
|
+
const likes = info.Upvotes || 0;
|
|
207
|
+
const bgStyle = displayCover ? `background-image: url('${displayCover}');` : `background: ${generateGradient(displayTitle)};`;
|
|
208
|
+
let summary = stripHtml(info.Content);
|
|
209
|
+
if (summary.length < 10 && parent && isChapter) summary = stripHtml(parent.Content);
|
|
210
|
+
if (summary.length > 100) summary = summary.substring(0, 100) + "...";
|
|
211
|
+
if (!summary) summary = "No Introduction";
|
|
212
|
+
const tagsArr = [];
|
|
213
|
+
if (displayTagsObj?.Type) tagsArr.push(displayTagsObj.Type);
|
|
214
|
+
if (displayTagsObj?.Rating && displayTagsObj.Rating !== "E") tagsArr.push(displayTagsObj.Rating);
|
|
215
|
+
if (displayTagsObj?.OtherTags && Array.isArray(displayTagsObj.OtherTags)) {
|
|
216
|
+
tagsArr.push(...displayTagsObj.OtherTags);
|
|
217
|
+
}
|
|
218
|
+
const displayTags = tagsArr.slice(0, 10);
|
|
219
|
+
const html = `
|
|
220
|
+
<!DOCTYPE html>
|
|
221
|
+
<html>
|
|
222
|
+
<head>
|
|
223
|
+
<style>
|
|
224
|
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
|
|
225
|
+
body { margin: 0; padding: 0; font-family: 'Noto Sans SC', sans-serif; background: transparent; }
|
|
226
|
+
.card { width: 600px; height: 320px; background: #fff; border-radius: 16px; box-shadow: 0 10px 30px rgba(0,0,0,0.15); display: flex; overflow: hidden; }
|
|
227
|
+
.cover { width: 220px; height: 100%; ${bgStyle} background-size: cover; background-position: center; position: relative; flex-shrink: 0; }
|
|
228
|
+
.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; }
|
|
229
|
+
.info { flex: 1; padding: 24px; display: flex; flex-direction: column; justify-content: space-between; overflow: hidden; }
|
|
230
|
+
|
|
231
|
+
.title {
|
|
232
|
+
font-size: 22px; font-weight: 700; color: #333; line-height: 1.35;
|
|
233
|
+
display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
|
|
234
|
+
}
|
|
235
|
+
.subtitle {
|
|
236
|
+
font-size: 15px; color: #555; margin-top: 6px; font-weight: 500;
|
|
237
|
+
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
|
238
|
+
padding-left: 10px; border-left: 3px solid #e91e63;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.author { font-size: 13px; color: #888; margin-top: 8px; font-weight: 400; }
|
|
242
|
+
|
|
243
|
+
.tags { display: flex; flex-wrap: wrap; gap: 6px; margin: 12px 0; max-height: 56px; overflow: hidden; }
|
|
244
|
+
.tag { background: #eff2f5; color: #5c6b7f; padding: 3px 9px; border-radius: 4px; font-size: 11px; font-weight: 500; }
|
|
245
|
+
.tag-imp { background: #e3f2fd; color: #1565c0; }
|
|
246
|
+
|
|
247
|
+
.summary {
|
|
248
|
+
font-size: 13px; color: #666; line-height: 1.6;
|
|
249
|
+
display: -webkit-box; -webkit-line-clamp: 4; -webkit-box-orient: vertical; overflow: hidden;
|
|
250
|
+
margin-top: auto;
|
|
251
|
+
}
|
|
252
|
+
.footer { border-top: 1px solid #eee; padding-top: 14px; display: flex; justify-content: space-between; font-size: 12px; color: #888; margin-top: 15px; }
|
|
253
|
+
.stat b { color: #555; font-weight: bold; margin-right: 2px;}
|
|
254
|
+
</style>
|
|
255
|
+
</head>
|
|
256
|
+
<body>
|
|
257
|
+
<div class="card">
|
|
258
|
+
<div class="cover"><div class="id-tag">ID: ${info.ID}</div></div>
|
|
259
|
+
<div class="info">
|
|
260
|
+
<div>
|
|
261
|
+
<div class="title">${displayTitle}</div>
|
|
262
|
+
${subTitle ? `<div class="subtitle">${subTitle}</div>` : ""}
|
|
263
|
+
<div class="author">@${info.UserName}</div>
|
|
264
|
+
<div class="tags">${displayTags.map((t) => `<span class="tag ${["文", "译", "R"].includes(t) ? "tag-imp" : ""}">${t}</span>`).join("")}</div>
|
|
265
|
+
</div>
|
|
266
|
+
<div class="summary">${summary}</div>
|
|
267
|
+
<div class="footer">
|
|
268
|
+
<span class="stat"><b style="color:#009688">热度</b>${views}</span>
|
|
269
|
+
<span class="stat"><b style="color:#673ab7">评论</b>${comments}</span>
|
|
270
|
+
<span class="stat"><b style="color:#4caf50">赞</b>${likes}</span>
|
|
271
|
+
<span class="stat"><b style="color:#795548">字数</b>${words}</span>
|
|
202
272
|
</div>
|
|
203
273
|
</div>
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
274
|
+
</div>
|
|
275
|
+
</body></html>`;
|
|
276
|
+
const page = await ctx.puppeteer.page();
|
|
277
|
+
await injectCookies(page);
|
|
278
|
+
await page.setContent(html);
|
|
279
|
+
await page.setViewport({ width: 640, height: 440, deviceScaleFactor: 2 });
|
|
280
|
+
const el = await page.$(".card");
|
|
281
|
+
const img = await el.screenshot({ type: "png" });
|
|
282
|
+
await page.close();
|
|
283
|
+
return img;
|
|
284
|
+
}, "renderCard");
|
|
285
|
+
const renderSearchResults = /* @__PURE__ */ __name(async (keyword, results) => {
|
|
286
|
+
const html = `
|
|
287
|
+
<!DOCTYPE html>
|
|
288
|
+
<html>
|
|
289
|
+
<head>
|
|
290
|
+
<style>
|
|
291
|
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
|
|
292
|
+
body { margin: 0; padding: 0; font-family: 'Noto Sans SC', sans-serif; width: 500px; background: transparent; }
|
|
293
|
+
.container { background: #fff; border-radius: 12px; overflow: hidden; box-shadow: 0 4px 20px rgba(0,0,0,0.15); margin: 10px; }
|
|
294
|
+
.header { background: #fafafa; padding: 15px 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
|
295
|
+
.header-title { font-size: 16px; font-weight: bold; color: #333; }
|
|
296
|
+
.list { padding: 0; }
|
|
297
|
+
.item { display: flex; padding: 15px; border-bottom: 1px solid #f5f5f5; height: 110px; align-items: flex-start; }
|
|
298
|
+
.cover-box { width: 75px; height: 100%; border-radius: 6px; overflow: hidden; flex-shrink: 0; margin-right: 15px; background: #eee; position: relative; }
|
|
299
|
+
.cover-img { width: 100%; height: 100%; object-fit: cover; }
|
|
300
|
+
.content { flex: 1; display: flex; flex-direction: column; justify-content: space-between; height: 100%; min-width: 0; }
|
|
301
|
+
.top-row { display: flex; justify-content: space-between; align-items: flex-start; }
|
|
302
|
+
.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; margin-bottom: 4px; flex:1; margin-right: 8px;}
|
|
303
|
+
.id-badge { background: #455a64; color: #fff; padding: 2px 6px; border-radius: 4px; font-family: monospace; font-size: 11px; font-weight: bold; flex-shrink: 0; }
|
|
304
|
+
.author { font-size: 12px; color: #666; }
|
|
305
|
+
.tags { display: flex; gap: 4px; flex-wrap: wrap; height: 18px; overflow: hidden; margin-top: 4px; }
|
|
306
|
+
.tag { background: #f3f3f3; color: #666; padding: 0 5px; border-radius: 3px; font-size: 10px; white-space: nowrap; line-height: 1.6;}
|
|
307
|
+
.meta-row { display: flex; gap: 10px; font-size: 11px; color: #999; margin-top: auto; border-top: 1px dashed #eee; padding-top: 5px; }
|
|
308
|
+
.stat b { margin-right: 1px; font-weight: bold; }
|
|
309
|
+
</style>
|
|
310
|
+
</head>
|
|
311
|
+
<body>
|
|
312
|
+
<div class="container">
|
|
313
|
+
<div class="header"><div class="header-title">🔍 "${keyword}"</div><div>Top ${results.length}</div></div>
|
|
314
|
+
<div class="list">
|
|
315
|
+
${results.map((r) => {
|
|
316
|
+
const bg = r.cover ? `<img class="cover-img" src="${r.cover}"/>` : `<div style="width:100%;height:100%;background:${generateGradient(r.title)}"></div>`;
|
|
317
|
+
const stats = [
|
|
318
|
+
r.stats.views && r.stats.views != "0" ? `<span class="stat" style="color:#009688"><b>热</b>${r.stats.views}</span>` : "",
|
|
319
|
+
r.stats.comments && r.stats.comments != "0" ? `<span class="stat" style="color:#673ab7"><b>评</b>${r.stats.comments}</span>` : "",
|
|
320
|
+
r.stats.likes && r.stats.likes != "0" ? `<span class="stat" style="color:#4caf50"><b>赞</b>${r.stats.likes}</span>` : "",
|
|
321
|
+
r.updateTime ? `<span class="stat" style="margin-left:auto;color:#757575">${r.updateTime}</span>` : ""
|
|
322
|
+
].join("");
|
|
323
|
+
return `<div class="item"><div class="cover-box">${bg}</div><div class="content">
|
|
324
|
+
<div class="top-row"><div class="title">${r.title}</div><div class="id-badge">ID: ${r.id}</div></div>
|
|
325
|
+
<div class="author">By ${r.author} ${r.status ? ` · ${r.status}` : ""}</div>
|
|
326
|
+
<div class="tags">${r.tags.map((t) => `<span class="tag">${t}</span>`).join("")}</div>
|
|
327
|
+
<div class="meta-row">${stats || "No Data"}</div>
|
|
328
|
+
</div></div>`;
|
|
329
|
+
}).join("")}
|
|
209
330
|
</div>
|
|
210
|
-
</
|
|
211
|
-
|
|
212
|
-
|
|
331
|
+
</div>
|
|
332
|
+
</body></html>`;
|
|
333
|
+
const page = await ctx.puppeteer.page();
|
|
334
|
+
await page.setContent(html);
|
|
335
|
+
await page.setViewport({ width: 550, height: 800, deviceScaleFactor: 2 });
|
|
336
|
+
await sleep(300);
|
|
337
|
+
const el = await page.$(".container");
|
|
338
|
+
const img = await el.screenshot({ type: "png" });
|
|
339
|
+
await page.close();
|
|
340
|
+
return img;
|
|
341
|
+
}, "renderSearchResults");
|
|
342
|
+
const renderReadPages = /* @__PURE__ */ __name(async (info) => {
|
|
343
|
+
const html = `
|
|
344
|
+
<!DOCTYPE html>
|
|
345
|
+
<html>
|
|
346
|
+
<head>
|
|
347
|
+
<style>
|
|
348
|
+
@import url('https://fonts.googleapis.com/css2?family=Noto+Serif+SC:wght@400;700&display=swap');
|
|
349
|
+
body { margin: 0; padding: 0; width: ${config.deviceWidth}px; background-color: #f6f4ec; color: #2c2c2c; font-family: 'Noto Serif SC', serif; }
|
|
350
|
+
#source-container { display: none; }
|
|
351
|
+
.page {
|
|
352
|
+
width: ${config.deviceWidth}px; height: ${config.deviceHeight}px;
|
|
353
|
+
padding: 35px 28px; box-sizing: border-box;
|
|
354
|
+
position: relative; background: #f6f4ec; overflow: hidden;
|
|
355
|
+
display: flex; flex-direction: column;
|
|
356
|
+
}
|
|
357
|
+
.page-header {
|
|
358
|
+
font-size: 12px; color: #8d6e63; border-bottom: 2px solid #d7ccc8;
|
|
359
|
+
padding-bottom: 12px; margin-bottom: 20px; flex-shrink: 0;
|
|
360
|
+
display: flex; justify-content: space-between; font-weight: bold;
|
|
361
|
+
}
|
|
362
|
+
.page-footer {
|
|
363
|
+
position: absolute; bottom: 15px; left: 0; right: 0; text-align: center;
|
|
364
|
+
font-size: 12px; color: #aaa; font-family: sans-serif;
|
|
365
|
+
}
|
|
366
|
+
.page-content { flex: 1; overflow: hidden; font-size: ${config.fontSize}px; line-height: 1.8; text-align: justify; }
|
|
367
|
+
p { margin: 0 0 1em 0; text-indent: 2em; }
|
|
368
|
+
img { max-width: 100%; height: auto; display: block; margin: 15px auto; border-radius: 6px; box-shadow: 0 4px 10px rgba(0,0,0,0.1); }
|
|
369
|
+
</style>
|
|
370
|
+
</head>
|
|
371
|
+
<body>
|
|
372
|
+
<div id="source-container"><div class="meta-info" data-title="${info.Title}" data-author="${info.UserName}"></div>${info.Content}</div>
|
|
373
|
+
<div id="output"></div>
|
|
374
|
+
<script>
|
|
375
|
+
function paginate() {
|
|
376
|
+
const source = document.getElementById('source-container');
|
|
377
|
+
const output = document.getElementById('output');
|
|
378
|
+
const title = source.querySelector('.meta-info').dataset.title;
|
|
379
|
+
const author = source.querySelector('.meta-info').dataset.author;
|
|
380
|
+
let pageIndex = 1;
|
|
381
|
+
let currentPageContent = null;
|
|
382
|
+
function createNewPage() {
|
|
383
|
+
const page = document.createElement('div'); page.className = 'page';
|
|
384
|
+
const header = document.createElement('div'); header.className = 'page-header';
|
|
385
|
+
header.innerHTML = \`<span>\${title.substring(0, 12) + (title.length>12?'...':'')}</span><span>\${author}</span>\`;
|
|
386
|
+
page.appendChild(header);
|
|
387
|
+
const content = document.createElement('div'); content.className = 'page-content';
|
|
388
|
+
page.appendChild(content);
|
|
389
|
+
const footer = document.createElement('div'); footer.className = 'page-footer';
|
|
390
|
+
footer.id = 'footer-' + pageIndex;
|
|
391
|
+
page.appendChild(footer);
|
|
392
|
+
output.appendChild(page);
|
|
393
|
+
currentPageContent = content;
|
|
394
|
+
return page;
|
|
395
|
+
}
|
|
396
|
+
createNewPage();
|
|
397
|
+
const children = Array.from(source.children);
|
|
398
|
+
for (const child of children) {
|
|
399
|
+
if (child.className === 'meta-info') continue;
|
|
400
|
+
currentPageContent.appendChild(child.cloneNode(true));
|
|
401
|
+
if (currentPageContent.scrollHeight > currentPageContent.clientHeight) {
|
|
402
|
+
currentPageContent.removeChild(currentPageContent.lastChild);
|
|
403
|
+
pageIndex++;
|
|
404
|
+
createNewPage();
|
|
405
|
+
currentPageContent.appendChild(child.cloneNode(true));
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
for(let i=1; i<=pageIndex; i++) document.getElementById('footer-'+i).innerText = \`- \${i} / \${pageIndex} -\`;
|
|
409
|
+
return pageIndex;
|
|
410
|
+
}
|
|
411
|
+
</script>
|
|
412
|
+
</body></html>`;
|
|
413
|
+
const page = await ctx.puppeteer.page();
|
|
213
414
|
try {
|
|
214
|
-
|
|
215
|
-
await page.setContent(
|
|
216
|
-
await
|
|
217
|
-
|
|
218
|
-
|
|
415
|
+
await injectCookies(page);
|
|
416
|
+
await page.setContent(html);
|
|
417
|
+
await page.setViewport({ width: config.deviceWidth, height: config.deviceHeight, deviceScaleFactor: 3 });
|
|
418
|
+
await page.evaluate("paginate()");
|
|
419
|
+
const imgs = [];
|
|
420
|
+
const pages = await page.$$(".page");
|
|
421
|
+
for (const p of pages) {
|
|
422
|
+
imgs.push(await p.screenshot({ type: "jpeg", quality: 90 }));
|
|
423
|
+
}
|
|
424
|
+
return imgs;
|
|
425
|
+
} finally {
|
|
219
426
|
await page.close();
|
|
427
|
+
}
|
|
428
|
+
}, "renderReadPages");
|
|
429
|
+
ctx.command("ft.info <threadId:string>", "预览作品").action(async ({ session }, threadId) => {
|
|
430
|
+
if (!threadId) return "请输入ID";
|
|
431
|
+
const res = await fetchThread(threadId);
|
|
432
|
+
if (!res.valid) return `[Error] ${res.msg}`;
|
|
433
|
+
const img = await renderCard(res.data, res.parent);
|
|
434
|
+
return session.send(import_koishi.h.image(img, "image/png"));
|
|
435
|
+
});
|
|
436
|
+
ctx.command("ft.read <threadId:string>", "阅读章节").action(async ({ session }, threadId) => {
|
|
437
|
+
if (!threadId) return "请输入ID";
|
|
438
|
+
const res = await fetchThread(threadId);
|
|
439
|
+
if (!res.valid) return `[Error] 读取失败: ${res.msg}`;
|
|
440
|
+
await session.send(`[Reading] ${res.data.Title}...`);
|
|
441
|
+
try {
|
|
442
|
+
const cardImg = await renderCard(res.data, res.parent);
|
|
443
|
+
await session.send(import_koishi.h.image(cardImg, "image/png"));
|
|
444
|
+
const pages = await renderReadPages(res.data);
|
|
445
|
+
const nodes = pages.map((buf) => (0, import_koishi.h)("message", import_koishi.h.image(buf, "image/jpeg")));
|
|
446
|
+
const navs = [];
|
|
447
|
+
if (res.menu?.length) {
|
|
448
|
+
const idx = res.menu.findIndex((m) => m.ID.toString() === threadId);
|
|
449
|
+
if (idx > 0) navs.push(`[Prev] /ft.read ${res.menu[idx - 1].ID}`);
|
|
450
|
+
if (idx < res.menu.length - 1) navs.push(`[Next] /ft.read ${res.menu[idx + 1].ID}`);
|
|
451
|
+
}
|
|
452
|
+
if (navs.length) nodes.push((0, import_koishi.h)("message", import_koishi.h.text("Navigation:\n" + navs.join("\n"))));
|
|
453
|
+
return session.send((0, import_koishi.h)("message", { forward: true }, nodes));
|
|
220
454
|
} catch (e) {
|
|
221
455
|
ctx.logger("fimtale").error(e);
|
|
222
|
-
return "
|
|
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})`);
|
|
456
|
+
return "[Error] 渲染失败";
|
|
232
457
|
}
|
|
233
|
-
|
|
234
|
-
|
|
458
|
+
});
|
|
459
|
+
ctx.command("ft.random", "随机作品").action(async ({ session }) => {
|
|
460
|
+
const id = await fetchRandomId();
|
|
461
|
+
if (!id) return "[Error] 获取失败";
|
|
462
|
+
const res = await fetchThread(id);
|
|
463
|
+
if (!res.valid) return `[Error] ID:${id} 读取失败`;
|
|
464
|
+
const img = await renderCard(res.data, res.parent);
|
|
465
|
+
await session.send(import_koishi.h.image(img, "image/png"));
|
|
466
|
+
return `Tip: 发送 /ft.read ${res.data.ID} 阅读全文`;
|
|
467
|
+
});
|
|
468
|
+
ctx.command("ft.search <keyword:text>", "搜索作品").action(async ({ session }, keyword) => {
|
|
469
|
+
if (!keyword) return "请输入关键词";
|
|
470
|
+
await session.send("[Search] 搜索中...");
|
|
471
|
+
const results = await searchThreads(keyword);
|
|
472
|
+
if (!results.length) return "未找到结果。";
|
|
473
|
+
const img = await renderSearchResults(keyword, results);
|
|
474
|
+
await session.send(import_koishi.h.image(img, "image/png"));
|
|
475
|
+
return "Tip: 发送 /ft.read <ID> 阅读";
|
|
476
|
+
});
|
|
477
|
+
ctx.command("ft.sub <threadId:string>", "订阅").action(async ({ session }, threadId) => {
|
|
478
|
+
if (!/^\d+$/.test(threadId)) return "ID错误";
|
|
479
|
+
const exist = await ctx.database.get("fimtale_subs", { cid: session.cid, threadId });
|
|
480
|
+
if (exist.length) return "已订阅";
|
|
481
|
+
const res = await fetchThread(threadId);
|
|
482
|
+
if (!res.valid) return "帖子不存在";
|
|
483
|
+
await ctx.database.create("fimtale_subs", { cid: session.cid, threadId, lastCount: res.data.Comments, lastCheck: Date.now() });
|
|
484
|
+
await session.send("[Success] 订阅成功");
|
|
485
|
+
const img = await renderCard(res.data, res.parent);
|
|
486
|
+
return session.send(import_koishi.h.image(img, "image/png"));
|
|
487
|
+
});
|
|
488
|
+
ctx.command("ft.unsub <threadId:string>", "退订").action(async ({ session }, threadId) => {
|
|
489
|
+
const res = await ctx.database.remove("fimtale_subs", { cid: session.cid, threadId });
|
|
490
|
+
return res.matched ? "[OK] 已退订" : "未找到订阅";
|
|
491
|
+
});
|
|
492
|
+
ctx.middleware(async (session, next) => {
|
|
493
|
+
if (!config.autoParseLink) return next();
|
|
494
|
+
const match = session.content.match(/fimtale\.com\/t\/(\d+)/);
|
|
495
|
+
if (match && match[1] && session.userId !== session.selfId) {
|
|
496
|
+
const res = await fetchThread(match[1]);
|
|
497
|
+
if (res.valid) {
|
|
498
|
+
const img = await renderCard(res.data, res.parent);
|
|
499
|
+
session.send(import_koishi.h.image(img, "image/png"));
|
|
500
|
+
}
|
|
235
501
|
}
|
|
236
|
-
return
|
|
502
|
+
return next();
|
|
237
503
|
});
|
|
238
504
|
ctx.setInterval(async () => {
|
|
239
505
|
const subs = await ctx.database.get("fimtale_subs", {});
|
|
240
506
|
if (!subs.length) return;
|
|
241
|
-
const
|
|
242
|
-
for (const tid of
|
|
243
|
-
const
|
|
244
|
-
if (!
|
|
245
|
-
const targets = subs.filter((s) => s.threadId === tid && s.lastCount < data.
|
|
246
|
-
if (targets.length
|
|
247
|
-
const msg =
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
count: data.comments,
|
|
251
|
-
url: `https://fimtale.com/t/${tid}`
|
|
252
|
-
});
|
|
507
|
+
const tids = [...new Set(subs.map((s) => s.threadId))];
|
|
508
|
+
for (const tid of tids) {
|
|
509
|
+
const res = await fetchThread(tid);
|
|
510
|
+
if (!res.valid) continue;
|
|
511
|
+
const targets = subs.filter((s) => s.threadId === tid && s.lastCount < res.data.Comments);
|
|
512
|
+
if (targets.length) {
|
|
513
|
+
const msg = `[Update] ${res.data.Title} 更新了!
|
|
514
|
+
回复: ${res.data.Comments}
|
|
515
|
+
https://fimtale.com/t/${tid}`;
|
|
253
516
|
for (const sub of targets) {
|
|
254
517
|
try {
|
|
255
|
-
await ctx.broadcast([sub.cid], msg);
|
|
256
|
-
await ctx.database.set("fimtale_subs", { id: sub.id }, {
|
|
257
|
-
|
|
258
|
-
lastCheck: Date.now()
|
|
259
|
-
});
|
|
260
|
-
} catch (e) {
|
|
518
|
+
await ctx.broadcast([sub.cid], import_koishi.h.parse(msg));
|
|
519
|
+
await ctx.database.set("fimtale_subs", { id: sub.id }, { lastCount: res.data.Comments });
|
|
520
|
+
} catch {
|
|
261
521
|
}
|
|
262
522
|
}
|
|
263
523
|
}
|
|
264
524
|
}
|
|
265
525
|
}, 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
526
|
}
|
|
292
527
|
__name(apply, "apply");
|
|
293
528
|
// Annotate the CommonJS export names for ESM import in node:
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-fimtale-api",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "0.0.
|
|
3
|
+
"description": "Koishi插件,从fimtale搜索/订阅/随机获取小说/解析链接等",
|
|
4
|
+
"version": "0.0.3",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
|
8
8
|
"lib",
|
|
9
9
|
"dist"
|
|
10
10
|
],
|
|
11
|
+
"homepage": "https://github.com/muyni233/koishi-plugin-fimtale-api",
|
|
11
12
|
"license": "MIT",
|
|
12
13
|
"keywords": [
|
|
13
14
|
"chatbot",
|
|
14
15
|
"koishi",
|
|
15
|
-
"plugin"
|
|
16
|
+
"plugin",
|
|
17
|
+
"fiction"
|
|
16
18
|
],
|
|
17
19
|
"peerDependencies": {
|
|
18
20
|
"koishi": "^4.18.7"
|
package/readme.md
CHANGED