koishi-plugin-mediawiki-fix 0.0.1 → 0.0.2

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,160 @@ 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) {
277
+ const label = escapeMarkdownText(text);
278
+ if (!this.config.enableQQInlineCmd) return label;
279
+ const commandText = `[[${title}]]`;
280
+ return ` [${label}](mqqapi://aio/inlinecmd?command=${encodeURIComponent(commandText)}&enter=false&reply=false)`;
281
+ }
282
+ formatLink(text, url) {
283
+ return ` [${escapeMarkdownText(text)}](${url})`;
284
+ }
285
+ resolveWikiTitleFromHref(mwApi, href) {
286
+ try {
287
+ const base = new URL(getUrl(mwApi));
288
+ const url = new URL(href, base);
289
+ if (url.origin !== base.origin) return "";
290
+ const titleFromSearch = url.searchParams.get("title");
291
+ if (titleFromSearch) {
292
+ return decodeURIComponent(titleFromSearch).replace(/_/g, " ");
293
+ }
294
+ const wikiPath = url.pathname.match(/\/wiki\/(.+)$/);
295
+ if (wikiPath?.[1]) {
296
+ return decodeURIComponent(wikiPath[1]).replace(/_/g, " ");
297
+ }
298
+ const path = decodeURIComponent(url.pathname).replace(/^\/+|\/+$/g, "");
299
+ if (path && !path.includes(":") && !path.includes("/") && !/\.(php|js|css|jpg|jpeg|png|gif|svg|webp)$/i.test(path)) {
300
+ return path.replace(/_/g, " ");
301
+ }
302
+ } catch {
303
+ }
304
+ return "";
305
+ }
306
+ convertHtmlExtractToMarkdown(session, mwApi, extract) {
307
+ const replaced = extract.replace(
308
+ /<a\b[^>]*href="([^"]+)"[^>]*>(.*?)<\/a>/gis,
309
+ (_, href, inner) => {
310
+ const text = decodeHtmlEntities(inner.replace(/<[^>]+>/g, "")).trim();
311
+ if (!text) return "";
312
+ const wikiTitle = this.resolveWikiTitleFromHref(mwApi, href);
313
+ if (wikiTitle) {
314
+ return this.formatAutoFillWikilink(session, wikiTitle, text);
315
+ }
316
+ try {
317
+ return this.formatLink(text, new URL(href, getUrl(mwApi)).href);
318
+ } catch {
319
+ return escapeMarkdownText(text);
320
+ }
321
+ }
322
+ );
323
+ return decodeHtmlEntities(
324
+ replaced.replace(/<br\s*\/?>/gi, "\n").replace(/<\/p>\s*<p[^>]*>/gi, "\n\n").replace(/<[^>]+>/g, "").replace(/\n{3,}/g, "\n\n").trim()
325
+ );
326
+ }
327
+ extractIntroHtmlFromParse(html) {
328
+ const cleaned = html.replace(/<style\b[^>]*>.*?<\/style>/gis, "").replace(/<script\b[^>]*>.*?<\/script>/gis, "");
329
+ const paragraphs = Array.from(cleaned.matchAll(/<p\b[^>]*>(.*?)<\/p>/gis)).map((match) => match[1].trim()).filter((content) => decodeHtmlEntities(content.replace(/<[^>]+>/g, "").trim()));
330
+ if (paragraphs.length) {
331
+ return `<p>${paragraphs[0]}</p>`;
332
+ }
333
+ return cleaned;
334
+ }
335
+ async fetchMarkdownExtracts(session, mwApi, titles) {
336
+ if (!titles.length) return /* @__PURE__ */ new Map();
337
+ const entries = await Promise.all(
338
+ titles.map(async (title) => {
339
+ const url = `${mwApi}?${new URLSearchParams({
340
+ action: "parse",
341
+ page: title,
342
+ prop: "text",
343
+ section: "0",
344
+ format: "json"
345
+ })}`;
346
+ const response = await fetch(url, {
347
+ headers: MOCK_HEADERS.find((item) => item.match(new URL(mwApi)))?.headers
348
+ });
349
+ const data = await response.json();
350
+ const parsedTitle = data.parse?.title;
351
+ const parsedHtml = data.parse?.text?.["*"];
352
+ if (!parsedTitle || !parsedHtml) return null;
353
+ const introHtml = this.extractIntroHtmlFromParse(parsedHtml);
354
+ return [
355
+ parsedTitle,
356
+ this.convertHtmlExtractToMarkdown(session, mwApi, introHtml)
357
+ ];
358
+ })
359
+ );
360
+ const result = new Map(entries.filter(Boolean));
361
+ this.logger.info("QQ_MARKDOWN_EXTRACTS", Object.fromEntries(result.entries()));
362
+ return result;
363
+ }
364
+ async sendTextOrMarkdown(session, title, text) {
365
+ this.logger.info("QQ_MARKDOWN_PAYLOAD", { title, text });
366
+ if (this.shouldUseQQMarkdown(session)) {
367
+ const internal = session.bot?.internal;
368
+ if (internal) {
369
+ session["seq"] = session["seq"] || 0;
370
+ const msgSeq = ++session["seq"];
371
+ const payload = {
372
+ content: title,
373
+ msg_type: 2,
374
+ msg_id: session.messageId,
375
+ msg_seq: msgSeq,
376
+ markdown: { content: text }
377
+ };
378
+ try {
379
+ if (session.isDirect) {
380
+ await internal.sendPrivateMessage(session.channelId, payload);
381
+ } else {
382
+ await internal.sendMessage(session.channelId, payload);
383
+ }
384
+ return "";
385
+ } catch (error) {
386
+ this.logger.warn("QQ native markdown send failed, fallback to text", error);
387
+ }
388
+ }
389
+ }
390
+ return text;
391
+ }
392
+ formatWikiQueryMarkdown(session, pageMsgs, interwiki) {
393
+ const blocks = [...pageMsgs];
394
+ for (const item of interwiki || []) {
395
+ blocks.push(`跨语言链接:
396
+ ${item.url}`);
397
+ }
398
+ return blocks.join("\n\n");
399
+ }
400
+ formatWikiSearchMarkdown(session, mwApi, keywords, totalhits, pages) {
401
+ const lines = [
402
+ `🔍关键词“${escapeMarkdownText(keywords)}”共匹配到 ${totalhits || 0} 个相关结果,我来简单整理一下前 ${pages.length} 个结果:`
403
+ ];
404
+ pages.sort((a, b) => a.index - b.index).forEach((item, index) => {
405
+ lines.push("");
406
+ lines.push(`(${index + 1}) ${this.formatInlineCmd(session, `wiki -d ${item.title}`, item.title)}`);
407
+ if (item.extract) {
408
+ lines.push(escapeMarkdownText(item.extract));
409
+ }
410
+ lines.push(getUrl(mwApi, { curid: String(item.pageid) }));
411
+ });
412
+ return lines.join("\n");
413
+ }
244
414
  #initCommands() {
245
415
  this.ctx.command("wiki [titles:text]", "MediaWiki 相关功能", {
246
416
  authority: this.config.cmdAuthWiki
@@ -299,6 +469,14 @@ var PluginMediawiki = class {
299
469
  });
300
470
  this.logger.debug("QUERY DATA", data.query);
301
471
  const { pages, redirects, interwiki, specialpagealiases, namespaces } = data.query;
472
+ const markdownExtracts = this.shouldUseQQMarkdown(session) && options?.details ? await this.fetchMarkdownExtracts(
473
+ session,
474
+ mwApi,
475
+ (pages || []).map((item) => item.title)
476
+ ).catch((error) => {
477
+ this.logger.warn("Failed to fetch markdown extracts", error);
478
+ return /* @__PURE__ */ new Map();
479
+ }) : /* @__PURE__ */ new Map();
302
480
  const dangerPageNames = ["Mypage", "Mytalk"];
303
481
  const dangerPages = specialpagealiases.filter((i) => dangerPageNames.includes(i.realname)).map((i) => i.aliases).flat(Infinity);
304
482
  const specialNsName = namespaces["-1"].name;
@@ -349,9 +527,9 @@ var PluginMediawiki = class {
349
527
  );
350
528
  } else if (missing !== void 0) {
351
529
  if (!options?.search) {
352
- msg.push(`${editurl} (💔页面不存在)`);
530
+ msg.push(`${this.formatLink("创建页面", editurl)} (💔页面不存在)`);
353
531
  } else {
354
- msg.push(`${editurl}
532
+ msg.push(`${this.formatLink("创建页面", editurl)}
355
533
  💡页面不存在,即将搜索wiki……`);
356
534
  }
357
535
  } else {
@@ -360,8 +538,18 @@ var PluginMediawiki = class {
360
538
  (shortUrl.length <= canonicalurl.length ? shortUrl : canonicalurl) + pageAnchor
361
539
  );
362
540
  }
363
- if (options?.details && page.extract) {
364
- msg.push(page.extract);
541
+ if (options?.details) {
542
+ const markdownExtract = markdownExtracts.get(pagetitle);
543
+ const finalExtract = markdownExtract || page.extract;
544
+ this.logger.info("QQ_MARKDOWN_PAGE_EXTRACT", {
545
+ title: pagetitle,
546
+ hasMarkdownExtract: !!markdownExtract,
547
+ markdownExtract,
548
+ fallbackExtract: page.extract
549
+ });
550
+ if (finalExtract) {
551
+ msg.push(finalExtract);
552
+ }
365
553
  }
366
554
  return msg.join("\n");
367
555
  }) || [];
@@ -370,7 +558,12 @@ var PluginMediawiki = class {
370
558
  }) || [];
371
559
  const allMsgList = [...pageMsgs, ...interwikiMsgs];
372
560
  let finalMsg = "";
373
- if (allMsgList.length === 1) {
561
+ let sentImmediately = false;
562
+ if (this.shouldUseQQMarkdown(session)) {
563
+ const markdownMsg = this.formatWikiQueryMarkdown(session, pageMsgs, interwiki);
564
+ finalMsg = await this.sendTextOrMarkdown(session, "Wiki 查询结果", markdownMsg);
565
+ sentImmediately = !finalMsg;
566
+ } else if (allMsgList.length === 1) {
374
567
  finalMsg = import_koishi.h.quote(session.messageId) + allMsgList[0];
375
568
  } else if (allMsgList.length > 1) {
376
569
  const msgBuilder = new BulkMessageBuilder(session);
@@ -380,16 +573,20 @@ var PluginMediawiki = class {
380
573
  finalMsg = msgBuilder.prependOriginal().all();
381
574
  }
382
575
  if (pages && pages.length === 1 && pages[0].ns === 0 && !pages[0].missing && !pages[0].invalid && options?.details) {
383
- await session.send(finalMsg);
576
+ if (!sentImmediately && finalMsg) {
577
+ await session.send(finalMsg);
578
+ }
384
579
  const infoboxImage = await this.shotInfobox(pages[0].canonicalurl, true);
385
580
  if (infoboxImage) {
386
581
  await session.send(infoboxImage);
387
582
  }
388
583
  } else if (options?.search && pages.length === 1 && pages[0].ns === 0 && pages[0].missing && !pages[0].invalid) {
389
- await session.send(finalMsg);
584
+ if (!sentImmediately && finalMsg) {
585
+ await session.send(finalMsg);
586
+ }
390
587
  await session.execute(`wiki.search ${pages[0].title}`);
391
588
  } else {
392
- return finalMsg;
589
+ return sentImmediately ? "" : finalMsg;
393
590
  }
394
591
  });
395
592
  this.ctx.middleware(async (session, next) => {
@@ -460,25 +657,36 @@ var PluginMediawiki = class {
460
657
  gsrnamespace: "0",
461
658
  gsrlimit: "5"
462
659
  });
463
- const bulk = new BulkMessageBuilder(session);
464
660
  if (search.length < 1) {
465
661
  return `💔找不到与“${keywords}”匹配的结果。`;
466
662
  } else if (search.length === 1) {
467
663
  return session.execute(`wiki -d ${search[0].title}`);
664
+ } else if (this.shouldUseQQMarkdown(session)) {
665
+ const markdownMsg = this.formatWikiSearchMarkdown(
666
+ session,
667
+ mwApi,
668
+ keywords,
669
+ searchinfo?.totalhits,
670
+ pages
671
+ );
672
+ const out = await this.sendTextOrMarkdown(session, "Wiki 搜索结果", markdownMsg);
673
+ if (out) return out;
674
+ return "";
468
675
  } else {
676
+ const bulk = new BulkMessageBuilder(session);
469
677
  bulk.prependOriginal();
470
678
  bulk.botSay(
471
679
  `🔍关键词“${keywords}”共匹配到 ${searchinfo?.totalhits || "∅"} 个相关结果,我来简单整理一下前 ${search.length} 个结果:`
472
680
  );
473
- }
474
- pages.sort((a, b) => a.index - b.index).forEach((item, index) => {
475
- bulk.botSay(
476
- `(${index + 1}) ${item.title}
681
+ pages.sort((a, b) => a.index - b.index).forEach((item, index) => {
682
+ bulk.botSay(
683
+ `(${index + 1}) ${item.title}
477
684
  ${item.extract}
478
685
  ${getUrl(session.channel.mwApi, { curid: String(item.pageid) })}`
479
- );
480
- });
481
- return bulk.all();
686
+ );
687
+ });
688
+ return bulk.all();
689
+ }
482
690
  });
483
691
  }
484
692
  async shotInfobox(url, silence = false) {
@@ -554,14 +762,19 @@ ${getUrl(session.channel.mwApi, { curid: String(item.pageid) })}`
554
762
  }) {
555
763
  const selectorString = Array.isArray(selector) ? selector.join(", ") : selector;
556
764
  return `
557
- body > *:not(#content),
558
765
  #mw-navigation,
559
766
  #mw-page-base,
560
767
  #mw-head-base,
561
- .mw-indicators {
768
+ .mw-indicators,
769
+ #firstHeading,
770
+ .printfooter,
771
+ .catlinks,
772
+ #catlinks,
773
+ .after-content,
774
+ .mw-editsection {
562
775
  display: none !important;
563
776
  }
564
- html, body, #content, .mw-parser-output {
777
+ html, body, .mw-body, .mw-body-content, #content, #mw-content-text, .mw-parser-output {
565
778
  visibility: visible !important;
566
779
  background: white !important;
567
780
  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.2",
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"