koishi-plugin-mediawiki-fix 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,159 @@
1
+ /**
2
+ * koishi-plugin-mediawiki
3
+ * @desc MediaWiki plugin for Koishijs
4
+ * @author Koishijs(机智的小鱼君) <dragon-fish@qq.com>
5
+ * @license Apache-2.0
6
+ */
7
+ import { Context, h, Logger, Schema } from 'koishi';
8
+ export interface InfoboxDefinition {
9
+ match: ((url: URL) => boolean) | string;
10
+ selector: string | string[];
11
+ injectStyles?: string;
12
+ skin?: string;
13
+ }
14
+ export interface Config {
15
+ defaultApiUrl?: string;
16
+ cmdAuthWiki: number;
17
+ cmdAuthConnect: number;
18
+ cmdAuthSearch: number;
19
+ searchIfNotExist: boolean;
20
+ showDetailsByDefault: boolean;
21
+ customInfoboxes: InfoboxDefinition[];
22
+ }
23
+ export type MWPages = MWPage[];
24
+ export interface MWPage {
25
+ pageid: number;
26
+ ns: number;
27
+ title: string;
28
+ extract: string;
29
+ contentmodel: string;
30
+ pagelanguage: string;
31
+ pagelanguagehtmlcode: string;
32
+ pagelanguagedir: string;
33
+ touched: string;
34
+ lastrevid: number;
35
+ length: number;
36
+ fullurl: string;
37
+ editurl: string;
38
+ canonicalurl: string;
39
+ displaytitle: string;
40
+ special?: boolean;
41
+ invalid?: boolean;
42
+ invalidreason?: string;
43
+ missing?: boolean;
44
+ }
45
+ export type MWRedirects = MWRedirect[];
46
+ export interface MWRedirect {
47
+ from: string;
48
+ to: string;
49
+ tofragment?: string;
50
+ }
51
+ export type MWInterwikiLinks = MWInterwiki[];
52
+ export interface MWInterwiki {
53
+ title: string;
54
+ iw: string;
55
+ url: string;
56
+ }
57
+ export type MWSpecialPageAliases = MWSpecialPageAlias[];
58
+ export interface MWSpecialPageAlias {
59
+ realname: string;
60
+ aliases: string[];
61
+ }
62
+ export type MWNamespaceAliases = MWNamespaceAlias[];
63
+ export interface MWNamespaceAlias {
64
+ id: number;
65
+ alias: string;
66
+ }
67
+ export type MWNamespaces = Record<string, MWNamespace>;
68
+ export interface MWNamespace {
69
+ id: number;
70
+ case: string;
71
+ name: string;
72
+ subpages: boolean;
73
+ canonical: string;
74
+ content: boolean;
75
+ nonincludable: boolean;
76
+ }
77
+ export interface MWApiResponseQueryPagesWithSiteinfo {
78
+ query: {
79
+ pages: MWPages;
80
+ redirects?: MWRedirects;
81
+ interwiki?: MWInterwikiLinks;
82
+ specialpagealiases: MWSpecialPageAliases;
83
+ namespacealiases: MWNamespaceAliases;
84
+ namespaces: MWNamespaces;
85
+ };
86
+ }
87
+ export interface MWApiResponsQueryPagesGeneratedBySearch {
88
+ query: {
89
+ searchinfo: {
90
+ totalhits: number;
91
+ };
92
+ pages: {
93
+ pageid: number;
94
+ ns: number;
95
+ title: string;
96
+ index: number;
97
+ extract: string;
98
+ }[];
99
+ search: {
100
+ ns: number;
101
+ title: string;
102
+ pageid: number;
103
+ }[];
104
+ };
105
+ }
106
+ declare module 'koishi' {
107
+ interface Channel {
108
+ mwApi?: string;
109
+ }
110
+ }
111
+ export declare const name = "mediawiki";
112
+ export default class PluginMediawiki {
113
+ #private;
114
+ ctx: Context;
115
+ config: Partial<Config>;
116
+ static inject: string[];
117
+ INFOBOX_DEFINITION: InfoboxDefinition[];
118
+ static Config: Schema<Schemastery.ObjectS<{
119
+ defaultApiUrl: Schema<string, string>;
120
+ cmdAuthWiki: Schema<number, number>;
121
+ cmdAuthConnect: Schema<number, number>;
122
+ cmdAuthSearch: Schema<number, number>;
123
+ searchIfNotExist: Schema<boolean, boolean>;
124
+ showDetailsByDefault: Schema<boolean, boolean>;
125
+ customInfoboxes: Schema<Schemastery.ObjectS<{
126
+ match: Schema<string, string>;
127
+ selector: Schema<string[], string[]>;
128
+ injectStyles: Schema<string, string>;
129
+ skin: Schema<string, string>;
130
+ }>[], Schemastery.ObjectT<{
131
+ match: Schema<string, string>;
132
+ selector: Schema<string[], string[]>;
133
+ injectStyles: Schema<string, string>;
134
+ skin: Schema<string, string>;
135
+ }>[]>;
136
+ }>, Schemastery.ObjectT<{
137
+ defaultApiUrl: Schema<string, string>;
138
+ cmdAuthWiki: Schema<number, number>;
139
+ cmdAuthConnect: Schema<number, number>;
140
+ cmdAuthSearch: Schema<number, number>;
141
+ searchIfNotExist: Schema<boolean, boolean>;
142
+ showDetailsByDefault: Schema<boolean, boolean>;
143
+ customInfoboxes: Schema<Schemastery.ObjectS<{
144
+ match: Schema<string, string>;
145
+ selector: Schema<string[], string[]>;
146
+ injectStyles: Schema<string, string>;
147
+ skin: Schema<string, string>;
148
+ }>[], Schemastery.ObjectT<{
149
+ match: Schema<string, string>;
150
+ selector: Schema<string[], string[]>;
151
+ injectStyles: Schema<string, string>;
152
+ skin: Schema<string, string>;
153
+ }>[]>;
154
+ }>>;
155
+ constructor(ctx: Context, config?: Partial<Config>);
156
+ get logger(): Logger;
157
+ shotInfobox(url: string, silence?: boolean): Promise<string | h>;
158
+ createInjectStylesFromDefinition({ selector, injectStyles, }: InfoboxDefinition): string;
159
+ }
package/lib/index.js ADDED
@@ -0,0 +1,588 @@
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
+ default: () => PluginMediawiki,
24
+ name: () => name
25
+ });
26
+ module.exports = __toCommonJS(src_exports);
27
+ var import_koishi = require("koishi");
28
+ var import_wiki_saikou = require("wiki-saikou");
29
+ var INFOBOX_DEFINITION = [
30
+ // 萌娘百科
31
+ {
32
+ match: /* @__PURE__ */ __name((url) => url.host.endsWith("moegirl.org.cn"), "match"),
33
+ selector: [
34
+ ".mw-parser-output .infotemplatebox",
35
+ ".mw-parser-output table.infobox2",
36
+ ".mw-parser-output table.infoboxSpecial",
37
+ ".mw-parser-output table.infobox"
38
+ ],
39
+ injectStyles: `
40
+ .mw-parser-output .infotemplatebox {
41
+ margin: 1rem !important;
42
+ }
43
+ `,
44
+ skin: "apioutput"
45
+ },
46
+ // Minecraft Wiki
47
+ {
48
+ match: /* @__PURE__ */ __name((url) => url.host === "minecraft.fandom.com", "match"),
49
+ selector: [".mw-parser-output .notaninfobox"],
50
+ skin: "apioutput"
51
+ },
52
+ // Fandom (basic)
53
+ {
54
+ match: /* @__PURE__ */ __name((url) => url.host.endsWith("fandom.com"), "match"),
55
+ selector: [".mw-parser-output aside.portable-infobox"],
56
+ skin: "apioutput"
57
+ },
58
+ // 万界规划局
59
+ {
60
+ match: /* @__PURE__ */ __name((url) => url.host.endsWith("wjghj.cn"), "match"),
61
+ selector: [".mw-parser-output .portable-infobox:not(.pi-theme-顶部提示小)"],
62
+ skin: "apioutput"
63
+ },
64
+ // 最终幻想XIV中文维基
65
+ {
66
+ match: /* @__PURE__ */ __name((url) => url.host === "ff14.huijiwiki.com", "match"),
67
+ selector: [
68
+ ".mw-parser-output .infobox-item",
69
+ ".mw-parser-output .quest-frame",
70
+ ".mw-parser-output .instance-infobox",
71
+ ".mw-parser-output .ff14-infobox"
72
+ ]
73
+ }
74
+ ];
75
+ var DEFAULT_CONFIGS = {
76
+ cmdAuthWiki: 1,
77
+ cmdAuthConnect: 2,
78
+ cmdAuthSearch: 1,
79
+ searchIfNotExist: false,
80
+ showDetailsByDefault: false,
81
+ customInfoboxes: []
82
+ };
83
+ var MOCK_HEADERS = [
84
+ {
85
+ match: /* @__PURE__ */ __name((url) => url.hostname.includes("huijiwiki.com"), "match"),
86
+ headers: {
87
+ "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36 Edg/114.0.1823.67"
88
+ }
89
+ }
90
+ ];
91
+ function resolveBrackets(str) {
92
+ return str.replace(new RegExp("&#91;", "g"), "[").replace(new RegExp("&#93;", "g"), "]");
93
+ }
94
+ __name(resolveBrackets, "resolveBrackets");
95
+ function useApi(baseURL) {
96
+ const api = new import_wiki_saikou.MediaWikiApi(baseURL);
97
+ const matchedMockHeader = MOCK_HEADERS.find(
98
+ (item) => item.match(new URL(baseURL))
99
+ );
100
+ if (matchedMockHeader) {
101
+ api.request.interceptors.request.use((ctx) => {
102
+ ctx.headers = { ...ctx.headers, ...matchedMockHeader.headers };
103
+ return ctx;
104
+ });
105
+ }
106
+ return api;
107
+ }
108
+ __name(useApi, "useApi");
109
+ function getWikiTitleDBKey(raw) {
110
+ const title = raw.replace(/[\s_]+/g, " ").trim().replace(/\s+/g, "_");
111
+ return title[0].toUpperCase() + title.slice(1);
112
+ }
113
+ __name(getWikiTitleDBKey, "getWikiTitleDBKey");
114
+ function getWikiDisplayTitle(raw) {
115
+ const title = raw.replace(/[\s_]+/g, " ").trim();
116
+ return title[0].toUpperCase() + title.slice(1);
117
+ }
118
+ __name(getWikiDisplayTitle, "getWikiDisplayTitle");
119
+ function parseTitlesFromText(str) {
120
+ str = resolveBrackets(str);
121
+ const reg = /\[\[(.+?)\]\]/g;
122
+ return Array.from(
123
+ new Set(
124
+ Array.from(str.matchAll(reg)).map((i) => i[1].split("|")[0]).map(getWikiTitleDBKey)
125
+ )
126
+ ).filter((i) => !!i);
127
+ }
128
+ __name(parseTitlesFromText, "parseTitlesFromText");
129
+ function getUrl(base, params = {}, script = "index") {
130
+ const query = Object.keys(params).length ? "?" + new URLSearchParams(params) : "";
131
+ return `${base.replace(
132
+ "/api.php",
133
+ `/${script ? script.trim() : "index"}.php`
134
+ )}${query}`;
135
+ }
136
+ __name(getUrl, "getUrl");
137
+ function isValidApi(api) {
138
+ let url;
139
+ try {
140
+ url = new URL(api);
141
+ } catch (err) {
142
+ return false;
143
+ }
144
+ const { protocol, pathname } = url;
145
+ if (protocol.startsWith("http") && pathname.endsWith("/api.php")) {
146
+ return true;
147
+ }
148
+ return false;
149
+ }
150
+ __name(isValidApi, "isValidApi");
151
+ var BulkMessageBuilder = class {
152
+ constructor(session) {
153
+ this.session = session;
154
+ this.#content = session.content || "";
155
+ this.#bot = {
156
+ userId: this.session.bot.userId,
157
+ nickname: this.session.bot.user?.name || "BOT"
158
+ };
159
+ this.#author = {
160
+ userId: session.userId,
161
+ nickname: session.username
162
+ };
163
+ }
164
+ static {
165
+ __name(this, "BulkMessageBuilder");
166
+ }
167
+ #figure = (0, import_koishi.h)("message", { forward: true });
168
+ #bot;
169
+ #author;
170
+ #content;
171
+ #isPrependOriginal = false;
172
+ all() {
173
+ return this.#figure;
174
+ }
175
+ get figure() {
176
+ return this.#figure;
177
+ }
178
+ addLine(author, message) {
179
+ this.#figure.children.push((0, import_koishi.h)("message", author, message));
180
+ return this;
181
+ }
182
+ botSay(msg) {
183
+ this.addLine(this.#bot, msg);
184
+ return this;
185
+ }
186
+ authorSay(msg) {
187
+ this.addLine(this.#author, msg);
188
+ return this;
189
+ }
190
+ prependOriginal() {
191
+ if (this.#isPrependOriginal) return this;
192
+ this.#isPrependOriginal = true;
193
+ this.#figure.children.unshift((0, import_koishi.h)("message", this.#author, this.#content));
194
+ return this;
195
+ }
196
+ };
197
+ var name = "mediawiki";
198
+ var PluginMediawiki = class {
199
+ constructor(ctx, config = DEFAULT_CONFIGS) {
200
+ this.ctx = ctx;
201
+ this.config = config;
202
+ this.config = { ...DEFAULT_CONFIGS, ...config };
203
+ this.INFOBOX_DEFINITION = [
204
+ ...this.config.customInfoboxes || [],
205
+ ...INFOBOX_DEFINITION
206
+ ];
207
+ ctx.model.extend("channel", {
208
+ mwApi: "string"
209
+ });
210
+ this.#initCommands();
211
+ }
212
+ static {
213
+ __name(this, "PluginMediawiki");
214
+ }
215
+ static inject = ["database", "puppeteer"];
216
+ INFOBOX_DEFINITION;
217
+ static Config = import_koishi.Schema.object({
218
+ defaultApiUrl: import_koishi.Schema.string().description("全局默认的 MediaWiki API 地址。当群聊未绑定特定 Wiki 时,将使用此地址。"),
219
+ cmdAuthWiki: import_koishi.Schema.number().description("指令`wiki`的权限等级:基础指令,请求条目链接与基本信息等").default(1),
220
+ cmdAuthConnect: import_koishi.Schema.number().description("指令`wiki.connect`的权限等级:将wiki绑定到群聊").default(2),
221
+ cmdAuthSearch: import_koishi.Schema.number().description("指令`wiki.search`的权限等级:在绑定的wiki中搜索").default(1),
222
+ searchIfNotExist: import_koishi.Schema.boolean().description(
223
+ "触发`wiki`指令时,结果有且仅有一个不存在的主名字空间的页面时否自动触发搜索"
224
+ ),
225
+ showDetailsByDefault: import_koishi.Schema.boolean().description(
226
+ "触发`wiki`指令时,是否默认附带页面摘要和信息框截图"
227
+ ),
228
+ customInfoboxes: import_koishi.Schema.array(
229
+ import_koishi.Schema.object({
230
+ match: import_koishi.Schema.string().description(
231
+ "正则表达式,决定该组信息框定义是否匹配当前请求的URL。(URL示例 `https://example.com/wiki/PageName`,填写示例:`^https?://example\\\\\\\\.com/`)"
232
+ ).required(),
233
+ selector: import_koishi.Schema.array(String).description("信息框的选择器").required(),
234
+ injectStyles: import_koishi.Schema.string().description("额外插入的CSS").role("textarea"),
235
+ skin: import_koishi.Schema.string().description(
236
+ "渲染时使用的皮肤,建议配置为 `apioutput` 提高加载速度"
237
+ )
238
+ })
239
+ ).description("自定义信息框定义组,每一个定义组至少需要match以及selector")
240
+ });
241
+ get logger() {
242
+ return this.ctx.logger("mediawiki");
243
+ }
244
+ #initCommands() {
245
+ this.ctx.command("wiki [titles:text]", "MediaWiki 相关功能", {
246
+ authority: this.config.cmdAuthWiki
247
+ }).example("wiki 页面 - 获取页面链接").channelFields(["mwApi"]).option("details", "-d 显示页面的更多资讯", {
248
+ type: "boolean",
249
+ fallback: this.config.showDetailsByDefault
250
+ }).option("search", "-s 如果页面不存在就进行搜索", {
251
+ type: "boolean",
252
+ fallback: this.config.searchIfNotExist
253
+ }).option("quiet", "-q 静默执行(忽略未绑定提示)", {
254
+ type: "boolean",
255
+ // @ts-ignore
256
+ hidden: true
257
+ }).action(async ({ session, options }, titlesInput = "") => {
258
+ if (!session?.channel) {
259
+ return "该指令需要在群聊(频道)中使用。";
260
+ }
261
+ let mwApi = session.channel.mwApi;
262
+ if (!mwApi) {
263
+ if (this.config.defaultApiUrl) {
264
+ mwApi = this.config.defaultApiUrl;
265
+ } else {
266
+ return options?.quiet ? "" : session.execute("wiki.connect -h");
267
+ }
268
+ }
269
+ if (!titlesInput) {
270
+ return getUrl(mwApi);
271
+ }
272
+ const api = useApi(mwApi);
273
+ const titles = Array.from(
274
+ new Set(titlesInput.split("|").map(getWikiDisplayTitle))
275
+ ).map((i) => {
276
+ return {
277
+ name: i.split("#")[0],
278
+ anchor: i.split("#")[1] ? "#" + encodeURI(i.split("#")[1]) : ""
279
+ };
280
+ }).filter((i) => !!i.name).slice(0, 5);
281
+ const { data } = await api.get({
282
+ action: "query",
283
+ prop: "extracts|info",
284
+ meta: "siteinfo",
285
+ siprop: "specialpagealiases|namespacealiases|namespaces",
286
+ iwurl: 1,
287
+ titles: titles.map((i) => i.name),
288
+ redirects: 1,
289
+ converttitles: 1,
290
+ exchars: "120",
291
+ exlimit: "max",
292
+ explaintext: 1,
293
+ exintro: 1,
294
+ exsectionformat: "plain",
295
+ inprop: "url|displaytitle"
296
+ }).catch((e) => {
297
+ session.send(`查询时遇到问题:${e || "-"}`);
298
+ throw e;
299
+ });
300
+ this.logger.debug("QUERY DATA", data.query);
301
+ const { pages, redirects, interwiki, specialpagealiases, namespaces } = data.query;
302
+ const dangerPageNames = ["Mypage", "Mytalk"];
303
+ const dangerPages = specialpagealiases.filter((i) => dangerPageNames.includes(i.realname)).map((i) => i.aliases).flat(Infinity);
304
+ const specialNsName = namespaces["-1"].name;
305
+ const pageMsgs = pages?.map((page) => {
306
+ const msg = [];
307
+ let pageRedirect = redirects?.find(({ to }) => to === page.title);
308
+ let pageAnchor = titles.find(
309
+ (i) => i.name.toLocaleLowerCase() === page.title.toLocaleLowerCase()
310
+ )?.anchor || "";
311
+ if (pageRedirect && pageRedirect.from.split(":")[0] === specialNsName && dangerPages.includes(
312
+ pageRedirect.from.split(":")?.[1].split("/")[0] || ""
313
+ )) {
314
+ page = {
315
+ ...page,
316
+ ns: -1,
317
+ title: pageRedirect.from,
318
+ special: true
319
+ };
320
+ pageRedirect = void 0;
321
+ delete page.missing;
322
+ }
323
+ const {
324
+ pageid,
325
+ title: pagetitle,
326
+ missing,
327
+ invalid,
328
+ canonicalurl,
329
+ special,
330
+ editurl
331
+ } = page;
332
+ msg.push(`您要的“${pagetitle}”:`);
333
+ if (pageRedirect) {
334
+ const { from, to, tofragment } = pageRedirect || {};
335
+ msg.push(
336
+ `重定向:[${from}] → [${to}${tofragment ? "#" + tofragment : ""}]`
337
+ );
338
+ if (tofragment) pageAnchor = "#" + encodeURI(tofragment);
339
+ }
340
+ if (invalid !== void 0) {
341
+ msg.push(
342
+ `😟页面名称不合法:${JSON.stringify(page.invalidreason) || "原因未知"}`
343
+ );
344
+ } else if (special) {
345
+ msg.push(
346
+ `${getUrl(mwApi, {
347
+ title: pagetitle
348
+ })}${pageAnchor} (${missing ? "不存在的" : ""}特殊页面)`
349
+ );
350
+ } else if (missing !== void 0) {
351
+ if (!options?.search) {
352
+ msg.push(`${editurl} (💔页面不存在)`);
353
+ } else {
354
+ msg.push(`${editurl}
355
+ 💡页面不存在,即将搜索wiki……`);
356
+ }
357
+ } else {
358
+ const shortUrl = getUrl(mwApi, { curid: String(pageid) });
359
+ msg.push(
360
+ (shortUrl.length <= canonicalurl.length ? shortUrl : canonicalurl) + pageAnchor
361
+ );
362
+ }
363
+ if (options?.details && page.extract) {
364
+ msg.push(page.extract);
365
+ }
366
+ return msg.join("\n");
367
+ }) || [];
368
+ const interwikiMsgs = interwiki?.map((item) => {
369
+ return [`跨语言链接:`, item.url].join("\n");
370
+ }) || [];
371
+ const allMsgList = [...pageMsgs, ...interwikiMsgs];
372
+ let finalMsg = "";
373
+ if (allMsgList.length === 1) {
374
+ finalMsg = import_koishi.h.quote(session.messageId) + allMsgList[0];
375
+ } else if (allMsgList.length > 1) {
376
+ const msgBuilder = new BulkMessageBuilder(session);
377
+ allMsgList.forEach((i) => {
378
+ msgBuilder.botSay(i);
379
+ });
380
+ finalMsg = msgBuilder.prependOriginal().all();
381
+ }
382
+ if (pages && pages.length === 1 && pages[0].ns === 0 && !pages[0].missing && !pages[0].invalid && options?.details) {
383
+ await session.send(finalMsg);
384
+ const infoboxImage = await this.shotInfobox(pages[0].canonicalurl, true);
385
+ if (infoboxImage) {
386
+ await session.send(infoboxImage);
387
+ }
388
+ } else if (options?.search && pages.length === 1 && pages[0].ns === 0 && pages[0].missing && !pages[0].invalid) {
389
+ await session.send(finalMsg);
390
+ await session.execute(`wiki.search ${pages[0].title}`);
391
+ } else {
392
+ return finalMsg;
393
+ }
394
+ });
395
+ this.ctx.middleware(async (session, next) => {
396
+ await next();
397
+ if (!session.channel) return;
398
+ const titles = parseTitlesFromText(session.content || "");
399
+ if (!titles.length) {
400
+ return;
401
+ }
402
+ session.execute(`wiki -q ${titles.join("|")}`);
403
+ });
404
+ this.ctx.command("wiki.connect [api:string]", "将群聊与 MediaWiki 网站连接", {
405
+ authority: this.config.cmdAuthConnect
406
+ }).alias("wiki.link").channelFields(["mwApi"]).action(async ({ session }, api) => {
407
+ if (!session?.channel) {
408
+ return "该指令需要在群聊(频道)中用于绑定 Wiki。";
409
+ }
410
+ const { channel } = session;
411
+ if (!api) {
412
+ return channel.mwApi ? `本群已与 ${channel.mwApi} 连接~` : "本群未连接到 MediaWiki 网站,请使用“wiki.connect <api网址>”进行连接。";
413
+ } else if (isValidApi(api)) {
414
+ channel.mwApi = api;
415
+ await session.channel.$update();
416
+ return session.execute("wiki.connect");
417
+ } else {
418
+ return "输入的不是合法 api.php 网址。";
419
+ }
420
+ });
421
+ this.ctx.command("wiki.search [srsearch:text]", "在绑定的 Wiki 中搜索", {
422
+ authority: this.config.cmdAuthSearch
423
+ }).channelFields(["mwApi"]).action(async ({ session }, keywords) => {
424
+ if (!session?.channel) {
425
+ return "该指令需要在已绑定 Wiki 的群聊(频道)中使用。";
426
+ }
427
+ let mwApi = session.channel.mwApi;
428
+ if (!mwApi) {
429
+ if (this.config.defaultApiUrl) {
430
+ mwApi = this.config.defaultApiUrl;
431
+ } else {
432
+ return session.execute("wiki.connect -h");
433
+ }
434
+ }
435
+ if (!keywords) {
436
+ session.sendQueued("要搜索什么呢?(输入空行或句号取消)");
437
+ keywords = (await session.prompt(30 * 1e3)).trim();
438
+ if (!keywords || keywords === "." || keywords === "。") return "";
439
+ }
440
+ const api = useApi(mwApi);
441
+ const {
442
+ data: {
443
+ query: { searchinfo, search, pages }
444
+ }
445
+ } = await api.post({
446
+ action: "query",
447
+ prop: "extracts",
448
+ list: "search",
449
+ generator: "search",
450
+ exchars: "120",
451
+ exintro: 1,
452
+ explaintext: 1,
453
+ exsectionformat: "plain",
454
+ srsearch: keywords,
455
+ srnamespace: "0",
456
+ srlimit: "5",
457
+ srinfo: "totalhits",
458
+ srprop: "",
459
+ gsrsearch: keywords,
460
+ gsrnamespace: "0",
461
+ gsrlimit: "5"
462
+ });
463
+ const bulk = new BulkMessageBuilder(session);
464
+ if (search.length < 1) {
465
+ return `💔找不到与“${keywords}”匹配的结果。`;
466
+ } else if (search.length === 1) {
467
+ return session.execute(`wiki -d ${search[0].title}`);
468
+ } else {
469
+ bulk.prependOriginal();
470
+ bulk.botSay(
471
+ `🔍关键词“${keywords}”共匹配到 ${searchinfo?.totalhits || "∅"} 个相关结果,我来简单整理一下前 ${search.length} 个结果:`
472
+ );
473
+ }
474
+ pages.sort((a, b) => a.index - b.index).forEach((item, index) => {
475
+ bulk.botSay(
476
+ `(${index + 1}) ${item.title}
477
+ ${item.extract}
478
+ ${getUrl(session.channel.mwApi, { curid: String(item.pageid) })}`
479
+ );
480
+ });
481
+ return bulk.all();
482
+ });
483
+ }
484
+ async shotInfobox(url, silence = false) {
485
+ if (!this.ctx.puppeteer) {
486
+ return silence ? "" : "无法获取截图,请安装 puppeteer 插件后再试~";
487
+ }
488
+ const matched = this.INFOBOX_DEFINITION.find((i) => {
489
+ if (typeof i.match === "string") {
490
+ return new RegExp(i.match).test(url);
491
+ } else {
492
+ return i.match(new URL(url));
493
+ }
494
+ });
495
+ if (!matched) return "";
496
+ this.logger.info("SHOT_INFOBOX", url, matched.selector);
497
+ const start = Date.now();
498
+ const timeSpend = /* @__PURE__ */ __name(() => ((Date.now() - start) / 1e3).toFixed(3) + "s", "timeSpend");
499
+ const pageURL = new URL(url);
500
+ if (matched.skin) {
501
+ pageURL.searchParams.set("useskin", matched.skin);
502
+ }
503
+ let pageLoaded = false;
504
+ const page = await this.ctx.puppeteer.page();
505
+ await page.setViewport({ width: 960, height: 720 });
506
+ try {
507
+ await page.goto(pageURL.href, {
508
+ timeout: 15 * 1e3,
509
+ waitUntil: "networkidle0"
510
+ });
511
+ pageLoaded = true;
512
+ } catch (e) {
513
+ this.logger.warn("SHOT_INFOBOX", `Navigation timeout or failed: ${e}`);
514
+ const contentExists = await page.$(".mw-parser-output");
515
+ if (contentExists) {
516
+ this.logger.info("SHOT_INFOBOX", "Content exists, proceeding with screenshot.");
517
+ pageLoaded = true;
518
+ } else {
519
+ this.logger.warn("SHOT_INFOBOX", "Content not found after timeout.");
520
+ }
521
+ }
522
+ if (!pageLoaded) {
523
+ await page.close();
524
+ return "";
525
+ }
526
+ try {
527
+ await page.addStyleTag({
528
+ content: this.createInjectStylesFromDefinition(matched)
529
+ });
530
+ } catch (e) {
531
+ this.logger.warn("SHOT_INFOBOX", "Inject styles error", e);
532
+ }
533
+ try {
534
+ const selector = Array.isArray(matched.selector) ? matched.selector.join(", ") : matched.selector;
535
+ const target = await page.waitForSelector(selector, { timeout: 5e3 }).catch(() => null);
536
+ if (!target) {
537
+ this.logger.info("SHOT_INFOBOX", "Canceled", "Missing target or timed out waiting for selector:", selector);
538
+ await page.close();
539
+ return "";
540
+ }
541
+ const img = await target.screenshot({ type: "jpeg", quality: 85 });
542
+ this.logger.info("SHOT_INFOBOX", "OK", `(${timeSpend()})`);
543
+ await page.close();
544
+ return import_koishi.h.image(img, "image/jpeg");
545
+ } catch (e) {
546
+ this.logger.warn("SHOT_INFOBOX", "Failed during screenshot", e);
547
+ await page?.close();
548
+ return "";
549
+ }
550
+ }
551
+ createInjectStylesFromDefinition({
552
+ selector,
553
+ injectStyles
554
+ }) {
555
+ const selectorString = Array.isArray(selector) ? selector.join(", ") : selector;
556
+ return `
557
+ body > *:not(#content),
558
+ #mw-navigation,
559
+ #mw-page-base,
560
+ #mw-head-base,
561
+ .mw-indicators {
562
+ display: none !important;
563
+ }
564
+ html, body, #content, .mw-parser-output {
565
+ visibility: visible !important;
566
+ background: white !important;
567
+ min-height: auto !important;
568
+ }
569
+ .mw-parser-output > *:not(${selectorString}) {
570
+ visibility: hidden;
571
+ }
572
+ ${selectorString}, ${selectorString} * {
573
+ visibility: visible;
574
+ }
575
+ ${injectStyles || ""}
576
+ `;
577
+ }
578
+ };
579
+ // Annotate the CommonJS export names for ESM import in node:
580
+ 0 && (module.exports = {
581
+ name
582
+ });
583
+ /**
584
+ * koishi-plugin-mediawiki
585
+ * @desc MediaWiki plugin for Koishijs
586
+ * @author Koishijs(机智的小鱼君) <dragon-fish@qq.com>
587
+ * @license Apache-2.0
588
+ */
@@ -0,0 +1,5 @@
1
+ import { InfoboxDefinition } from './types/Infobox';
2
+ /**
3
+ * 内置匹配表,开发者也可以自己扩展该表
4
+ */
5
+ export declare const INFOBOX_DEFINITION: InfoboxDefinition[];
@@ -0,0 +1,9 @@
1
+ import { InfoboxDefinition } from './Infobox';
2
+ export interface Config {
3
+ cmdAuthWiki: number;
4
+ cmdAuthConnect: number;
5
+ cmdAuthSearch: number;
6
+ searchIfNotExist: boolean;
7
+ showDetailsByDefault: boolean;
8
+ customInfoboxes: InfoboxDefinition[];
9
+ }
@@ -0,0 +1,6 @@
1
+ export interface InfoboxDefinition {
2
+ match: (url: URL) => boolean | string;
3
+ selector: string | string[];
4
+ injectStyles?: string;
5
+ skin?: string;
6
+ }
@@ -0,0 +1,83 @@
1
+ export type MWPages = MWPage[];
2
+ export interface MWPage {
3
+ pageid: number;
4
+ ns: number;
5
+ title: string;
6
+ extract: string;
7
+ contentmodel: string;
8
+ pagelanguage: string;
9
+ pagelanguagehtmlcode: string;
10
+ pagelanguagedir: string;
11
+ touched: string;
12
+ lastrevid: number;
13
+ length: number;
14
+ fullurl: string;
15
+ editurl: string;
16
+ canonicalurl: string;
17
+ displaytitle: string;
18
+ special?: boolean;
19
+ invalid?: boolean;
20
+ invalidreason?: string;
21
+ missing?: boolean;
22
+ }
23
+ export type MWRedirects = MWRedirect[];
24
+ export interface MWRedirect {
25
+ from: string;
26
+ to: string;
27
+ tofragment?: string;
28
+ }
29
+ export type MWInterwikiLinks = MWInterwiki[];
30
+ export interface MWInterwiki {
31
+ title: string;
32
+ iw: string;
33
+ url: string;
34
+ }
35
+ export type MWSpecialPageAliases = MWSpecialPageAlias[];
36
+ export interface MWSpecialPageAlias {
37
+ realname: string;
38
+ aliases: string[];
39
+ }
40
+ export type MWNamespaceAliases = MWNamespaceAlias[];
41
+ export interface MWNamespaceAlias {
42
+ id: number;
43
+ alias: string;
44
+ }
45
+ export type MWNamespaces = Record<string, MWNamespace>;
46
+ export interface MWNamespace {
47
+ id: number;
48
+ case: string;
49
+ name: string;
50
+ subpages: boolean;
51
+ canonical: string;
52
+ content: boolean;
53
+ nonincludable: boolean;
54
+ }
55
+ export interface MWApiResponseQueryPagesWithSiteinfo {
56
+ query: {
57
+ pages: MWPages;
58
+ redirects?: MWRedirects;
59
+ interwiki?: MWInterwikiLinks;
60
+ specialpagealiases: MWSpecialPageAliases;
61
+ namespacealiases: MWNamespaceAliases;
62
+ namespaces: MWNamespaces;
63
+ };
64
+ }
65
+ export interface MWApiResponsQueryPagesGeneratedBySearch {
66
+ query: {
67
+ searchinfo: {
68
+ totalhits: number;
69
+ };
70
+ pages: {
71
+ pageid: number;
72
+ ns: number;
73
+ title: string;
74
+ index: number;
75
+ extract: string;
76
+ }[];
77
+ search: {
78
+ ns: number;
79
+ title: string;
80
+ pageid: number;
81
+ }[];
82
+ };
83
+ }
@@ -0,0 +1,14 @@
1
+ import { Session, Universal, h } from 'koishi';
2
+ type MsgUser = Omit<Universal.User & Universal.GuildMember, 'id'>;
3
+ export declare class BulkMessageBuilder {
4
+ #private;
5
+ session: Session;
6
+ constructor(session: Session);
7
+ all(): h;
8
+ get figure(): h;
9
+ addLine(author: MsgUser, message: string): this;
10
+ botSay(msg: string): this;
11
+ authorSay(msg: string): this;
12
+ prependOriginal(): this;
13
+ }
14
+ export {};
@@ -0,0 +1 @@
1
+ export declare function resolveBrackets(str: string): string;
@@ -0,0 +1,7 @@
1
+ import { MediaWikiApi } from 'wiki-saikou';
2
+ export declare function useApi(baseURL: string): MediaWikiApi;
3
+ export declare function getWikiTitleDBKey(raw: string): string;
4
+ export declare function getWikiDisplayTitle(raw: string): string;
5
+ export declare function parseTitlesFromText(str: string): string[];
6
+ export declare function getUrl(base: string, params?: {}, script?: string): string;
7
+ export declare function isValidApi(api: string | URL): boolean;
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "koishi-plugin-mediawiki-fix",
3
+ "description": "Koishi.js 的 MediaWiki 插件,将您的群聊与 wiki 站点紧密连接!(修复版)",
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
+ "scripts": {},
13
+ "dependencies": {
14
+ "tslib": "^2.7.0",
15
+ "wiki-saikou": "^3.4.0"
16
+ },
17
+ "devDependencies": {
18
+ "@types/node": "^22.6.1",
19
+ "@typescript-eslint/eslint-plugin": "^8.7.0",
20
+ "@typescript-eslint/parser": "^8.7.0",
21
+ "eslint": "^9.11.1",
22
+ "eslint-config-prettier": "^9.1.0",
23
+ "eslint-plugin-prettier": "^5.2.1",
24
+ "koishi": "^4.18.0",
25
+ "koishi-plugin-puppeteer": "^3.9.0",
26
+ "prettier": "^3.3.3",
27
+ "typescript": "^5.6.2"
28
+ },
29
+ "peerDependencies": {
30
+ "koishi": "^4.18.0",
31
+ "koishi-plugin-puppeteer": "^3.9.0"
32
+ },
33
+ "repository": "https://github.com/koishijs/koishi-plugin-mediawiki",
34
+ "keywords": [
35
+ "koishi",
36
+ "koishi-plugin",
37
+ "mediawiki",
38
+ "mediawiki-bot",
39
+ "mediawiki-api",
40
+ "subscribe",
41
+ "chatbot"
42
+ ]
43
+ }
package/readme.md ADDED
@@ -0,0 +1,5 @@
1
+ # koishi-plugin-mediawiki-fix
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-mediawiki-fix?style=flat-square)](https://www.npmjs.com/package/koishi-plugin-mediawiki-fix)
4
+
5
+ Koishi.js 的 MediaWiki 插件,将您的群聊与 wiki 站点紧密连接!(修复版)