koishi-plugin-mediawiki-fix 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 CHANGED
@@ -18,6 +18,8 @@ export interface Config {
18
18
  cmdAuthSearch: number;
19
19
  searchIfNotExist: boolean;
20
20
  showDetailsByDefault: boolean;
21
+ enableQQNativeMarkdown: boolean;
22
+ enableQQInlineCmd: boolean;
21
23
  customInfoboxes: InfoboxDefinition[];
22
24
  }
23
25
  export type MWPages = MWPage[];
@@ -122,6 +124,8 @@ export default class PluginMediawiki {
122
124
  cmdAuthSearch: Schema<number, number>;
123
125
  searchIfNotExist: Schema<boolean, boolean>;
124
126
  showDetailsByDefault: Schema<boolean, boolean>;
127
+ enableQQNativeMarkdown: Schema<boolean, boolean>;
128
+ enableQQInlineCmd: Schema<boolean, boolean>;
125
129
  customInfoboxes: Schema<Schemastery.ObjectS<{
126
130
  match: Schema<string, string>;
127
131
  selector: Schema<string[], string[]>;
@@ -140,6 +144,8 @@ export default class PluginMediawiki {
140
144
  cmdAuthSearch: Schema<number, number>;
141
145
  searchIfNotExist: Schema<boolean, boolean>;
142
146
  showDetailsByDefault: Schema<boolean, boolean>;
147
+ enableQQNativeMarkdown: Schema<boolean, boolean>;
148
+ enableQQInlineCmd: Schema<boolean, boolean>;
143
149
  customInfoboxes: Schema<Schemastery.ObjectS<{
144
150
  match: Schema<string, string>;
145
151
  selector: Schema<string[], string[]>;
@@ -154,6 +160,19 @@ export default class PluginMediawiki {
154
160
  }>>;
155
161
  constructor(ctx: Context, config?: Partial<Config>);
156
162
  get logger(): Logger;
163
+ private isQQSession;
164
+ private shouldUseQQMarkdown;
165
+ private getCommandText;
166
+ private formatInlineCmd;
167
+ private formatAutoFillWikilink;
168
+ private formatLink;
169
+ private resolveWikiTitleFromHref;
170
+ private convertHtmlExtractToMarkdown;
171
+ private extractIntroHtmlFromParse;
172
+ private fetchMarkdownExtracts;
173
+ private sendTextOrMarkdown;
174
+ private formatWikiQueryMarkdown;
175
+ private formatWikiSearchMarkdown;
157
176
  shotInfobox(url: string, silence?: boolean): Promise<string | h>;
158
177
  createInjectStylesFromDefinition({ selector, injectStyles, }: InfoboxDefinition): string;
159
178
  }
package/lib/index.js CHANGED
@@ -78,6 +78,8 @@ var DEFAULT_CONFIGS = {
78
78
  cmdAuthSearch: 1,
79
79
  searchIfNotExist: false,
80
80
  showDetailsByDefault: false,
81
+ enableQQNativeMarkdown: true,
82
+ enableQQInlineCmd: true,
81
83
  customInfoboxes: []
82
84
  };
83
85
  var MOCK_HEADERS = [
@@ -148,6 +150,14 @@ function isValidApi(api) {
148
150
  return false;
149
151
  }
150
152
  __name(isValidApi, "isValidApi");
153
+ function escapeMarkdownText(text) {
154
+ return text.replace(/[\[\]]/g, "\\$&");
155
+ }
156
+ __name(escapeMarkdownText, "escapeMarkdownText");
157
+ function decodeHtmlEntities(text) {
158
+ return text.replace(/&nbsp;/g, " ").replace(/&quot;/g, '"').replace(/&#39;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&").replace(/&#(\d+);/g, (_, code) => String.fromCodePoint(Number(code)));
159
+ }
160
+ __name(decodeHtmlEntities, "decodeHtmlEntities");
151
161
  var BulkMessageBuilder = class {
152
162
  constructor(session) {
153
163
  this.session = session;
@@ -225,6 +235,12 @@ var PluginMediawiki = class {
225
235
  showDetailsByDefault: import_koishi.Schema.boolean().description(
226
236
  "触发`wiki`指令时,是否默认附带页面摘要和信息框截图"
227
237
  ),
238
+ enableQQNativeMarkdown: import_koishi.Schema.boolean().description(
239
+ "是否在 QQ 平台优先使用官机原生 Markdown 发送查询结果。"
240
+ ).default(true),
241
+ enableQQInlineCmd: import_koishi.Schema.boolean().description(
242
+ "是否在 QQ 原生 Markdown 中启用 mqqapi 快捷点击指令。"
243
+ ).default(true),
228
244
  customInfoboxes: import_koishi.Schema.array(
229
245
  import_koishi.Schema.object({
230
246
  match: import_koishi.Schema.string().description(
@@ -241,6 +257,163 @@ var PluginMediawiki = class {
241
257
  get logger() {
242
258
  return this.ctx.logger("mediawiki");
243
259
  }
260
+ isQQSession(session) {
261
+ return session.platform === "qq" || session.bot?.platform === "qq";
262
+ }
263
+ shouldUseQQMarkdown(session) {
264
+ return this.config.enableQQNativeMarkdown && this.isQQSession(session);
265
+ }
266
+ getCommandText(session, command) {
267
+ const prefix = session.resolve(session.app.koishi.config.prefix)[0] ?? "";
268
+ return prefix ? `${prefix}${command}` : `/${command}`;
269
+ }
270
+ formatInlineCmd(session, command, text) {
271
+ const label = escapeMarkdownText(text);
272
+ if (!this.config.enableQQInlineCmd) return `\`${label}\``;
273
+ const commandText = this.getCommandText(session, command);
274
+ return ` [${label}](mqqapi://aio/inlinecmd?command=${encodeURIComponent(commandText)}&enter=true&reply=false) `;
275
+ }
276
+ formatAutoFillWikilink(session, title, text, missing = false) {
277
+ const label = escapeMarkdownText(text);
278
+ const display = missing ? `${label} (未创建)` : label;
279
+ if (!this.config.enableQQInlineCmd) return display;
280
+ const commandText = `[[${title}]]`;
281
+ return ` [${display}](mqqapi://aio/inlinecmd?command=${encodeURIComponent(commandText)}&enter=false&reply=false) `;
282
+ }
283
+ formatLink(text, url) {
284
+ return ` [${escapeMarkdownText(text)}](${url})`;
285
+ }
286
+ resolveWikiTitleFromHref(mwApi, href) {
287
+ try {
288
+ const base = new URL(getUrl(mwApi));
289
+ const url = new URL(href, base);
290
+ if (url.origin !== base.origin) return "";
291
+ const titleFromSearch = url.searchParams.get("title");
292
+ if (titleFromSearch) {
293
+ return decodeURIComponent(titleFromSearch).replace(/_/g, " ");
294
+ }
295
+ const wikiPath = url.pathname.match(/\/wiki\/(.+)$/);
296
+ if (wikiPath?.[1]) {
297
+ return decodeURIComponent(wikiPath[1]).replace(/_/g, " ");
298
+ }
299
+ const path = decodeURIComponent(url.pathname).replace(/^\/+|\/+$/g, "");
300
+ if (path && !path.includes(":") && !path.includes("/") && !/\.(php|js|css|jpg|jpeg|png|gif|svg|webp)$/i.test(path)) {
301
+ return path.replace(/_/g, " ");
302
+ }
303
+ } catch {
304
+ }
305
+ return "";
306
+ }
307
+ convertHtmlExtractToMarkdown(session, mwApi, extract) {
308
+ const replaced = extract.replace(
309
+ /<a\b([^>]*)href="([^"]+)"([^>]*)>(.*?)<\/a>/gis,
310
+ (_, beforeHref, href, afterHref, inner) => {
311
+ const text = decodeHtmlEntities(inner.replace(/<[^>]+>/g, "")).trim();
312
+ if (!text) return "";
313
+ const attrs = `${beforeHref} ${afterHref}`;
314
+ const isMissing = /\bnew\b/i.test(attrs) || /(?:\?|&)redlink=1\b/i.test(href);
315
+ const wikiTitle = this.resolveWikiTitleFromHref(mwApi, href);
316
+ if (wikiTitle) {
317
+ return this.formatAutoFillWikilink(session, wikiTitle, text, isMissing);
318
+ }
319
+ try {
320
+ return this.formatLink(text, new URL(href, getUrl(mwApi)).href);
321
+ } catch {
322
+ return escapeMarkdownText(text);
323
+ }
324
+ }
325
+ );
326
+ return decodeHtmlEntities(
327
+ replaced.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>\s*<p[^>]*>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/\n{3,}/g, "\n\n").trim()
328
+ );
329
+ }
330
+ extractIntroHtmlFromParse(html) {
331
+ const cleaned = html.replace(/<style\b[^>]*>.*?<\/style>/gis, "").replace(/<script\b[^>]*>.*?<\/script>/gis, "");
332
+ const paragraphs = Array.from(cleaned.matchAll(/<p\b[^>]*>(.*?)<\/p>/gis)).map((match) => match[1].trim()).filter((content) => decodeHtmlEntities(content.replace(/<[^>]+>/g, "").trim()));
333
+ if (paragraphs.length) {
334
+ return `<p>${paragraphs[0]}</p>`;
335
+ }
336
+ return cleaned;
337
+ }
338
+ async fetchMarkdownExtracts(session, mwApi, titles) {
339
+ if (!titles.length) return /* @__PURE__ */ new Map();
340
+ const entries = await Promise.all(
341
+ titles.map(async (title) => {
342
+ const url = `${mwApi}?${new URLSearchParams({
343
+ action: "parse",
344
+ page: title,
345
+ prop: "text",
346
+ section: "0",
347
+ format: "json"
348
+ })}`;
349
+ const response = await fetch(url, {
350
+ headers: MOCK_HEADERS.find((item) => item.match(new URL(mwApi)))?.headers
351
+ });
352
+ const data = await response.json();
353
+ const parsedTitle = data.parse?.title;
354
+ const parsedHtml = data.parse?.text?.["*"];
355
+ if (!parsedTitle || !parsedHtml) return null;
356
+ const introHtml = this.extractIntroHtmlFromParse(parsedHtml);
357
+ return [
358
+ parsedTitle,
359
+ this.convertHtmlExtractToMarkdown(session, mwApi, introHtml)
360
+ ];
361
+ })
362
+ );
363
+ const result = new Map(entries.filter(Boolean));
364
+ this.logger.info("QQ_MARKDOWN_EXTRACTS", Object.fromEntries(result.entries()));
365
+ return result;
366
+ }
367
+ async sendTextOrMarkdown(session, title, text) {
368
+ this.logger.info("QQ_MARKDOWN_PAYLOAD", { title, text });
369
+ if (this.shouldUseQQMarkdown(session)) {
370
+ const internal = session.bot?.internal;
371
+ if (internal) {
372
+ session["seq"] = session["seq"] || 0;
373
+ const msgSeq = ++session["seq"];
374
+ const payload = {
375
+ content: title,
376
+ msg_type: 2,
377
+ msg_id: session.messageId,
378
+ msg_seq: msgSeq,
379
+ markdown: { content: text }
380
+ };
381
+ try {
382
+ if (session.isDirect) {
383
+ await internal.sendPrivateMessage(session.channelId, payload);
384
+ } else {
385
+ await internal.sendMessage(session.channelId, payload);
386
+ }
387
+ return "";
388
+ } catch (error) {
389
+ this.logger.warn("QQ native markdown send failed, fallback to text", error);
390
+ }
391
+ }
392
+ }
393
+ return text;
394
+ }
395
+ formatWikiQueryMarkdown(session, pageMsgs, interwiki) {
396
+ const blocks = [...pageMsgs];
397
+ for (const item of interwiki || []) {
398
+ blocks.push(`跨语言链接:
399
+ ${item.url}`);
400
+ }
401
+ return blocks.join("\n\n");
402
+ }
403
+ formatWikiSearchMarkdown(session, mwApi, keywords, totalhits, pages) {
404
+ const lines = [
405
+ `🔍关键词“${escapeMarkdownText(keywords)}”共匹配到 ${totalhits || 0} 个相关结果,我来简单整理一下前 ${pages.length} 个结果:`
406
+ ];
407
+ pages.sort((a, b) => a.index - b.index).forEach((item, index) => {
408
+ lines.push("");
409
+ lines.push(`(${index + 1}) ${this.formatInlineCmd(session, `wiki -d ${item.title}`, item.title)}`);
410
+ if (item.extract) {
411
+ lines.push(escapeMarkdownText(item.extract));
412
+ }
413
+ lines.push(getUrl(mwApi, { curid: String(item.pageid) }));
414
+ });
415
+ return lines.join("\n");
416
+ }
244
417
  #initCommands() {
245
418
  this.ctx.command("wiki [titles:text]", "MediaWiki 相关功能", {
246
419
  authority: this.config.cmdAuthWiki
@@ -299,6 +472,14 @@ var PluginMediawiki = class {
299
472
  });
300
473
  this.logger.debug("QUERY DATA", data.query);
301
474
  const { pages, redirects, interwiki, specialpagealiases, namespaces } = data.query;
475
+ const markdownExtracts = this.shouldUseQQMarkdown(session) && options?.details ? await this.fetchMarkdownExtracts(
476
+ session,
477
+ mwApi,
478
+ (pages || []).map((item) => item.title)
479
+ ).catch((error) => {
480
+ this.logger.warn("Failed to fetch markdown extracts", error);
481
+ return /* @__PURE__ */ new Map();
482
+ }) : /* @__PURE__ */ new Map();
302
483
  const dangerPageNames = ["Mypage", "Mytalk"];
303
484
  const dangerPages = specialpagealiases.filter((i) => dangerPageNames.includes(i.realname)).map((i) => i.aliases).flat(Infinity);
304
485
  const specialNsName = namespaces["-1"].name;
@@ -349,9 +530,9 @@ var PluginMediawiki = class {
349
530
  );
350
531
  } else if (missing !== void 0) {
351
532
  if (!options?.search) {
352
- msg.push(`${editurl} (💔页面不存在)`);
533
+ msg.push(`${this.formatLink("创建页面", editurl)} (💔页面不存在)`);
353
534
  } else {
354
- msg.push(`${editurl}
535
+ msg.push(`${this.formatLink("创建页面", editurl)}
355
536
  💡页面不存在,即将搜索wiki……`);
356
537
  }
357
538
  } else {
@@ -360,8 +541,18 @@ var PluginMediawiki = class {
360
541
  (shortUrl.length <= canonicalurl.length ? shortUrl : canonicalurl) + pageAnchor
361
542
  );
362
543
  }
363
- if (options?.details && page.extract) {
364
- msg.push(page.extract);
544
+ if (options?.details) {
545
+ const markdownExtract = markdownExtracts.get(pagetitle);
546
+ const finalExtract = markdownExtract || page.extract;
547
+ this.logger.info("QQ_MARKDOWN_PAGE_EXTRACT", {
548
+ title: pagetitle,
549
+ hasMarkdownExtract: !!markdownExtract,
550
+ markdownExtract,
551
+ fallbackExtract: page.extract
552
+ });
553
+ if (finalExtract) {
554
+ msg.push(finalExtract);
555
+ }
365
556
  }
366
557
  return msg.join("\n");
367
558
  }) || [];
@@ -370,7 +561,12 @@ var PluginMediawiki = class {
370
561
  }) || [];
371
562
  const allMsgList = [...pageMsgs, ...interwikiMsgs];
372
563
  let finalMsg = "";
373
- if (allMsgList.length === 1) {
564
+ let sentImmediately = false;
565
+ if (this.shouldUseQQMarkdown(session)) {
566
+ const markdownMsg = this.formatWikiQueryMarkdown(session, pageMsgs, interwiki);
567
+ finalMsg = await this.sendTextOrMarkdown(session, "Wiki 查询结果", markdownMsg);
568
+ sentImmediately = !finalMsg;
569
+ } else if (allMsgList.length === 1) {
374
570
  finalMsg = import_koishi.h.quote(session.messageId) + allMsgList[0];
375
571
  } else if (allMsgList.length > 1) {
376
572
  const msgBuilder = new BulkMessageBuilder(session);
@@ -380,16 +576,20 @@ var PluginMediawiki = class {
380
576
  finalMsg = msgBuilder.prependOriginal().all();
381
577
  }
382
578
  if (pages && pages.length === 1 && pages[0].ns === 0 && !pages[0].missing && !pages[0].invalid && options?.details) {
383
- await session.send(finalMsg);
579
+ if (!sentImmediately && finalMsg) {
580
+ await session.send(finalMsg);
581
+ }
384
582
  const infoboxImage = await this.shotInfobox(pages[0].canonicalurl, true);
385
583
  if (infoboxImage) {
386
584
  await session.send(infoboxImage);
387
585
  }
388
586
  } else if (options?.search && pages.length === 1 && pages[0].ns === 0 && pages[0].missing && !pages[0].invalid) {
389
- await session.send(finalMsg);
587
+ if (!sentImmediately && finalMsg) {
588
+ await session.send(finalMsg);
589
+ }
390
590
  await session.execute(`wiki.search ${pages[0].title}`);
391
591
  } else {
392
- return finalMsg;
592
+ return sentImmediately ? "" : finalMsg;
393
593
  }
394
594
  });
395
595
  this.ctx.middleware(async (session, next) => {
@@ -460,25 +660,36 @@ var PluginMediawiki = class {
460
660
  gsrnamespace: "0",
461
661
  gsrlimit: "5"
462
662
  });
463
- const bulk = new BulkMessageBuilder(session);
464
663
  if (search.length < 1) {
465
664
  return `💔找不到与“${keywords}”匹配的结果。`;
466
665
  } else if (search.length === 1) {
467
666
  return session.execute(`wiki -d ${search[0].title}`);
667
+ } else if (this.shouldUseQQMarkdown(session)) {
668
+ const markdownMsg = this.formatWikiSearchMarkdown(
669
+ session,
670
+ mwApi,
671
+ keywords,
672
+ searchinfo?.totalhits,
673
+ pages
674
+ );
675
+ const out = await this.sendTextOrMarkdown(session, "Wiki 搜索结果", markdownMsg);
676
+ if (out) return out;
677
+ return "";
468
678
  } else {
679
+ const bulk = new BulkMessageBuilder(session);
469
680
  bulk.prependOriginal();
470
681
  bulk.botSay(
471
682
  `🔍关键词“${keywords}”共匹配到 ${searchinfo?.totalhits || "∅"} 个相关结果,我来简单整理一下前 ${search.length} 个结果:`
472
683
  );
473
- }
474
- pages.sort((a, b) => a.index - b.index).forEach((item, index) => {
475
- bulk.botSay(
476
- `(${index + 1}) ${item.title}
684
+ pages.sort((a, b) => a.index - b.index).forEach((item, index) => {
685
+ bulk.botSay(
686
+ `(${index + 1}) ${item.title}
477
687
  ${item.extract}
478
688
  ${getUrl(session.channel.mwApi, { curid: String(item.pageid) })}`
479
- );
480
- });
481
- return bulk.all();
689
+ );
690
+ });
691
+ return bulk.all();
692
+ }
482
693
  });
483
694
  }
484
695
  async shotInfobox(url, silence = false) {
@@ -554,14 +765,19 @@ ${getUrl(session.channel.mwApi, { curid: String(item.pageid) })}`
554
765
  }) {
555
766
  const selectorString = Array.isArray(selector) ? selector.join(", ") : selector;
556
767
  return `
557
- body > *:not(#content),
558
768
  #mw-navigation,
559
769
  #mw-page-base,
560
770
  #mw-head-base,
561
- .mw-indicators {
771
+ .mw-indicators,
772
+ #firstHeading,
773
+ .printfooter,
774
+ .catlinks,
775
+ #catlinks,
776
+ .after-content,
777
+ .mw-editsection {
562
778
  display: none !important;
563
779
  }
564
- html, body, #content, .mw-parser-output {
780
+ html, body, .mw-body, .mw-body-content, #content, #mw-content-text, .mw-parser-output {
565
781
  visibility: visible !important;
566
782
  background: white !important;
567
783
  min-height: auto !important;
@@ -5,5 +5,7 @@ export interface Config {
5
5
  cmdAuthSearch: number;
6
6
  searchIfNotExist: boolean;
7
7
  showDetailsByDefault: boolean;
8
+ enableQQNativeMarkdown: boolean;
9
+ enableQQInlineCmd: boolean;
8
10
  customInfoboxes: InfoboxDefinition[];
9
11
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-mediawiki-fix",
3
3
  "description": "Koishi.js 的 MediaWiki 插件,将您的群聊与 wiki 站点紧密连接!(修复版)",
4
- "version": "0.0.1",
4
+ "version": "0.0.3",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -9,7 +9,6 @@
9
9
  "dist"
10
10
  ],
11
11
  "license": "MIT",
12
- "scripts": {},
13
12
  "dependencies": {
14
13
  "tslib": "^2.7.0",
15
14
  "wiki-saikou": "^3.4.0"