koishi-plugin-dew-bot 1.0.251 → 1.0.252

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/README.md CHANGED
@@ -10,7 +10,3 @@
10
10
  踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
11
11
 
12
12
  无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
13
-
14
- ---
15
-
16
- 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
package/lib/index.js CHANGED
@@ -42,40 +42,6 @@ var import_koishi2 = require("koishi");
42
42
  // src/plugin/serverHandel/image.ts
43
43
  var import_koishi = require("koishi");
44
44
 
45
- // src/shared/bot/analysisMsg.ts
46
- var AnalysisMsg = class {
47
- static {
48
- __name(this, "AnalysisMsg");
49
- }
50
- parse(data) {
51
- let msg = "";
52
- let haveTitle = false;
53
- for (const item of data.line) {
54
- switch (item.type) {
55
- case 0 /* title */:
56
- haveTitle = true;
57
- msg += `┄━═${item.content}═━┄
58
- `;
59
- break;
60
- case 1 /* line */:
61
- msg += `${item.content}
62
- `;
63
- break;
64
- case 2 /* error */:
65
- msg += `❌出错了!错误原因:${item.content},请反馈至联系开发者
66
- `;
67
- break;
68
- default:
69
- break;
70
- }
71
- }
72
- if (haveTitle) {
73
- msg += "┄━════════━┄";
74
- }
75
- return msg;
76
- }
77
- };
78
-
79
45
  // src/plugin/appearanceCardTemplate.ts
80
46
  var escape = /* @__PURE__ */ __name((value) => String(value ?? "").replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]), "escape");
81
47
  function appearanceCardHtml(card, artwork, paper) {
@@ -689,28 +655,37 @@ var handel_base = class {
689
655
  }
690
656
  };
691
657
 
692
- // src/plugin/serverHandel/image.ts
693
- var ImageHandler = class extends handel_base {
658
+ // src/shared/bot/analysisMsg.ts
659
+ var AnalysisMsg = class {
694
660
  static {
695
- __name(this, "ImageHandler");
661
+ __name(this, "AnalysisMsg");
696
662
  }
697
- async start(data) {
698
- let content;
699
- let rendered = false;
700
- const renderStartedAt = this.timing ? performance.now() : 0;
701
- try {
702
- if (!data.characterCard && !data.itemCard && !data.card && !data.leaderboard && !data.equipmentCard) throw new Error("缺少卡片数据");
703
- const image = await renderSkillCard(this.ctx, data.card, data.leaderboard, data.itemCard, data.characterCard, data.equipmentCard);
704
- if (this.timing) this.timing.imageBytes = image.length;
705
- content = import_koishi.h.image(image, "image/jpeg").toString();
706
- rendered = true;
707
- } catch (error) {
708
- this.ctx.logger.warn("卡片绘制失败,使用文字详情", error instanceof Error ? error.message : "unknown");
709
- content = new AnalysisMsg().parse(data);
710
- } finally {
711
- if (this.timing) this.timing.renderMs = performance.now() - renderStartedAt;
663
+ parse(data) {
664
+ let msg = "";
665
+ let haveTitle = false;
666
+ for (const item of data.line) {
667
+ switch (item.type) {
668
+ case 0 /* title */:
669
+ haveTitle = true;
670
+ msg += `┄━═${item.content}═━┄
671
+ `;
672
+ break;
673
+ case 1 /* line */:
674
+ msg += `${item.content}
675
+ `;
676
+ break;
677
+ case 2 /* error */:
678
+ msg += `❌出错了!错误原因:${item.content},请反馈至联系开发者
679
+ `;
680
+ break;
681
+ default:
682
+ break;
683
+ }
684
+ }
685
+ if (haveTitle) {
686
+ msg += "┄━════════━┄";
712
687
  }
713
- await this.send(data, content, rendered ? "image" : "text");
688
+ return msg;
714
689
  }
715
690
  };
716
691
 
@@ -723,6 +698,12 @@ function compactDisplayText(value) {
723
698
  return value.trim().replace(/[ \t\u3000]*([·×+|])[ \t\u3000]*/g, "$1").replace(/([\p{Script=Han}\p{Extended_Pictographic}\p{Emoji_Modifier}\p{Regional_Indicator}\uFE0F])[ \t\u3000]+/gu, "$1").replace(/[ \t\u3000]+(?=[\p{Script=Han}\p{Extended_Pictographic}\p{Regional_Indicator}])/gu, "");
724
699
  }
725
700
  __name(compactDisplayText, "compactDisplayText");
701
+ function renderQQTitle(content) {
702
+ return `**${escapeMarkdown(compactDisplayText(content))}**
703
+
704
+ ***`;
705
+ }
706
+ __name(renderQQTitle, "renderQQTitle");
726
707
  function compactListLabel(value) {
727
708
  const label = compactDisplayText(value);
728
709
  return /[📖📚]/u.test(label) ? label.replace(/技能书(?=$|×\d+(?:\.\d+)?(?:||$)||)/g, "") : label;
@@ -733,6 +714,42 @@ function inlineCommandUrl(command) {
733
714
  return `mqqapi://aio/inlinecmd?command=${encoded}&enter=true&reply=false`;
734
715
  }
735
716
  __name(inlineCommandUrl, "inlineCommandUrl");
717
+ function inlineCommandSpans(content) {
718
+ const spans = [];
719
+ let depth = 0, start = 0, labelStart = 0;
720
+ for (let index = 0; index < content.length; index++) {
721
+ if (content[index] === "[") {
722
+ if (depth === 0) {
723
+ start = index > 0 && content[index - 1] === "\\" ? index - 1 : index;
724
+ labelStart = index + 1;
725
+ }
726
+ depth++;
727
+ } else if (content[index] === "]" && depth > 0 && --depth === 0) {
728
+ const label = content.slice(labelStart, index).replace(/\\$/, "");
729
+ if (!label.trim() || /[\[\]\r\n]/.test(label) || /^\d+$/.test(label.trim())) continue;
730
+ const command = label.split(/[++]/, 1)[0].trim();
731
+ if (command) spans.push({ start, end: index + 1, label, command });
732
+ }
733
+ }
734
+ return spans;
735
+ }
736
+ __name(inlineCommandSpans, "inlineCommandSpans");
737
+ function hasInlineCommands(content) {
738
+ return inlineCommandSpans(content).length > 0;
739
+ }
740
+ __name(hasInlineCommands, "hasInlineCommands");
741
+ function renderInlineCommands(content) {
742
+ const parts = [];
743
+ let offset = 0;
744
+ for (const span of inlineCommandSpans(content)) {
745
+ parts.push(escapeMarkdown(content.slice(offset, span.start)));
746
+ parts.push("[" + escapeMarkdown(span.label) + "](" + inlineCommandUrl(span.command) + ")");
747
+ offset = span.end;
748
+ }
749
+ parts.push(escapeMarkdown(content.slice(offset)));
750
+ return parts.join("");
751
+ }
752
+ __name(renderInlineCommands, "renderInlineCommands");
736
753
  function pageCommandLines(nav) {
737
754
  return [
738
755
  ...nav.previousCommand ? [nav.previousCommand] : [],
@@ -746,11 +763,6 @@ function renderQQPageMarkdown(data, showGreeting) {
746
763
  const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/;
747
764
  const pagePattern = /^第\s*\d+\s*\/\s*\d+\s*页(?:\s*[||]\s*共\s*(\d+)\s*([项条]))?$/;
748
765
  const title = data.line.find((item) => item.type === 0 /* title */);
749
- const summaryLine = data.line.find((item) => item.type === 1 /* line */ && pagePattern.test(item.content));
750
- const summary = summaryLine && pagePattern.exec(summaryLine.content);
751
- const listCount = data.line.filter((item) => item.type === 1 /* line */ && listPattern.test(item.content)).length;
752
- const count = summary?.[1] ?? (nav?.total === 1 && listCount > 0 ? String(listCount) : void 0);
753
- const countUnit = summary?.[2] ?? "项";
754
766
  const isLeaderboardDirectory = !!nav?.entries?.length && /^(?:🏆)?排行榜$/u.test(compactDisplayText(title?.content ?? ""));
755
767
  const sections = [];
756
768
  const bodyLines = [];
@@ -773,20 +785,16 @@ function renderQQPageMarkdown(data, showGreeting) {
773
785
  switch (item.type) {
774
786
  case 0 /* title */:
775
787
  flush();
776
- if (isLeaderboardDirectory) {
777
- sections.push(`## ${item.content.includes("🏆") ? "" : "🏆"}${escapeMarkdown(compactDisplayText(item.content))}`);
778
- sections.push(`> 共${nav.entries.length}个榜单·点击榜名查看`);
779
- } else {
780
- sections.push(`**${escapeMarkdown(compactDisplayText(item.content))}${count !== void 0 ? `·${count}${countUnit}` : ""}**`);
781
- }
788
+ sections.push(renderQQTitle(`${isLeaderboardDirectory && !item.content.includes("🏆") ? "🏆" : ""}${item.content}`));
782
789
  break;
783
790
  case 1 /* line */: {
784
791
  const listEntry = listPattern.exec(item.content);
785
792
  if (listEntry) {
786
793
  flushBody();
787
794
  const command2 = entryCommands.get(lineIndex);
788
- const label = escapeMarkdown(compactListLabel(listEntry[2]));
789
- const name2 = command2 ? `[${label}](${inlineCommandUrl(command2)})` : label;
795
+ const content = compactListLabel(listEntry[2]);
796
+ const label = escapeMarkdown(content);
797
+ const name2 = command2 ? `[${label}](${inlineCommandUrl(command2)})` : renderInlineCommands(content);
790
798
  listLines.push(`${listEntry[1]}. ${name2}`);
791
799
  break;
792
800
  }
@@ -803,17 +811,17 @@ function renderQQPageMarkdown(data, showGreeting) {
803
811
  const page = pagePattern.exec(item.content);
804
812
  if (page && nav) {
805
813
  flushBody();
806
- pageFooter = `**第${nav.current}/${nav.total}页${!title && page[1] ? `·共${page[1]}${page[2]}` : ""}**`;
814
+ pageFooter = `**第${nav.current}/${nav.total}页**`;
807
815
  hasPageSummary = true;
808
816
  break;
809
817
  }
810
818
  const command = entryCommands.get(lineIndex);
811
- bodyLines.push(command ? `[${escapeMarkdown(compactDisplayText(item.content))}](${inlineCommandUrl(command)})` : escapeMarkdown(item.content));
819
+ bodyLines.push(command ? `[${escapeMarkdown(compactListLabel(item.content))}](${inlineCommandUrl(command)})` : renderInlineCommands(item.content));
812
820
  break;
813
821
  }
814
822
  case 2 /* error */:
815
823
  flush();
816
- sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`);
824
+ sections.push(`❌ 出错了!错误原因:${renderInlineCommands(item.content)},请反馈至联系开发者`);
817
825
  break;
818
826
  }
819
827
  }
@@ -824,10 +832,7 @@ function renderQQPageMarkdown(data, showGreeting) {
824
832
  const nextLink = nav?.nextCommand ? `[${nav.nextCommand === "下一个" ? "下一个" : "下一页"}](${inlineCommandUrl(nav.nextCommand)})` : void 0;
825
833
  const helpLink = hasHelp ? `[指令](${inlineCommandUrl("指令")})` : void 0;
826
834
  const footer = pageFooter ? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join("·") : helpLink ? `更多操作:${helpLink}` : void 0;
827
- if (footer) {
828
- if (isLeaderboardDirectory) sections.push("***");
829
- sections.push(footer);
830
- }
835
+ if (footer) sections.push(footer);
831
836
  return sections.join("\n\n") || " ";
832
837
  }
833
838
  __name(renderQQPageMarkdown, "renderQQPageMarkdown");
@@ -883,12 +888,12 @@ function renderQQTransactionMarkdown(data, showGreeting) {
883
888
  for (const item of data.line) {
884
889
  if (item.type === 0 /* title */) {
885
890
  flush();
886
- sections.push(`**${escapeMarkdown(item.content)}**`);
891
+ sections.push(renderQQTitle(item.content));
887
892
  continue;
888
893
  }
889
894
  if (item.type === 2 /* error */) {
890
895
  flush();
891
- sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`);
896
+ sections.push(`❌ 出错了!错误原因:${renderInlineCommands(item.content)},请反馈至联系开发者`);
892
897
  continue;
893
898
  }
894
899
  if (item.type !== 1 /* line */) continue;
@@ -908,7 +913,7 @@ function renderQQTransactionMarkdown(data, showGreeting) {
908
913
  flushBody();
909
914
  sections.push(`**${escapeMarkdown(section?.[1] ?? "还缺")}**`);
910
915
  } else {
911
- bodyLines.push(escapeMarkdown(item.content));
916
+ bodyLines.push(renderInlineCommands(item.content));
912
917
  }
913
918
  }
914
919
  flush();
@@ -921,20 +926,6 @@ async function sendQQTransaction(session, data) {
921
926
  __name(sendQQTransaction, "sendQQTransaction");
922
927
 
923
928
  // src/plugin/serverHandel/qqCommandMessage.ts
924
- function renderCommandLine(content, commandHelp) {
925
- if (!commandHelp || !/^\s*\[[^\[\]\r\n]+\]/.test(content)) return escapeMarkdown(content);
926
- const parts = [];
927
- let offset = 0;
928
- for (const match of content.matchAll(/\[([^\[\]\r\n]+)\]/g)) {
929
- parts.push(escapeMarkdown(content.slice(offset, match.index)));
930
- const command = match[1].split(/[++]/, 1)[0].trim();
931
- parts.push(command ? `[${escapeMarkdown(match[1])}](${inlineCommandUrl(command)})` : escapeMarkdown(match[0]));
932
- offset = match.index + match[0].length;
933
- }
934
- parts.push(escapeMarkdown(content.slice(offset)));
935
- return parts.join("");
936
- }
937
- __name(renderCommandLine, "renderCommandLine");
938
929
  function renderQQCommandMarkdown(data, showGreeting) {
939
930
  const sections = [];
940
931
  const bodyLines = [];
@@ -946,15 +937,15 @@ function renderQQCommandMarkdown(data, showGreeting) {
946
937
  switch (item.type) {
947
938
  case 0 /* title */:
948
939
  flushBody();
949
- sections.push(`**${escapeMarkdown(item.content)}**`);
940
+ sections.push(renderQQTitle(item.content));
950
941
  break;
951
942
  case 1 /* line */:
952
943
  if (!item.content.trim()) flushBody();
953
- else bodyLines.push(renderCommandLine(item.content, !!data.commandHelp));
944
+ else bodyLines.push(renderInlineCommands(item.content));
954
945
  break;
955
946
  case 2 /* error */:
956
947
  flushBody();
957
- sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`);
948
+ sections.push(`❌ 出错了!错误原因:${renderInlineCommands(item.content)},请反馈至联系开发者`);
958
949
  break;
959
950
  }
960
951
  }
@@ -967,6 +958,57 @@ async function sendQQCommand(session, data) {
967
958
  }
968
959
  __name(sendQQCommand, "sendQQCommand");
969
960
 
961
+ // src/plugin/serverHandel/qqStoryMessage.ts
962
+ function hasStoryCodeBlock(data) {
963
+ return data.line.some((item) => item.type === 1 /* line */ && item.codeBlock === true);
964
+ }
965
+ __name(hasStoryCodeBlock, "hasStoryCodeBlock");
966
+ function fencedStory(content) {
967
+ const story = content.replace(/\r\n?/g, "\n");
968
+ const longestRun = Math.max(0, ...Array.from(story.matchAll(/`+/g), (match) => match[0].length));
969
+ const fence = "`".repeat(Math.max(3, longestRun + 1));
970
+ return fence + "\n" + story + (story.endsWith("\n") ? "" : "\n") + fence;
971
+ }
972
+ __name(fencedStory, "fencedStory");
973
+ function renderQQStoryMarkdown(data, showGreeting) {
974
+ const sections = [];
975
+ const bodyLines = [];
976
+ const flushBody = /* @__PURE__ */ __name(() => {
977
+ if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
978
+ }, "flushBody");
979
+ if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
980
+ for (const item of data.line) {
981
+ switch (item.type) {
982
+ case 0 /* title */:
983
+ flushBody();
984
+ sections.push(renderQQTitle(item.content));
985
+ break;
986
+ case 1 /* line */:
987
+ if (item.codeBlock) {
988
+ flushBody();
989
+ sections.push(fencedStory(item.content));
990
+ } else {
991
+ for (const line of item.content.replace(/\r\n?/g, "\n").split("\n")) {
992
+ if (!line.trim()) flushBody();
993
+ else bodyLines.push(renderInlineCommands(line));
994
+ }
995
+ }
996
+ break;
997
+ case 2 /* error */:
998
+ flushBody();
999
+ sections.push("❌ 出错了!错误原因:" + renderInlineCommands(item.content) + ",请反馈至联系开发者");
1000
+ break;
1001
+ }
1002
+ }
1003
+ flushBody();
1004
+ return sections.join("\n\n") || " ";
1005
+ }
1006
+ __name(renderQQStoryMarkdown, "renderQQStoryMarkdown");
1007
+ async function sendQQStory(session, data) {
1008
+ await sendQQMarkdown(session, renderQQStoryMarkdown(data, session.event?.channel?.type === 0));
1009
+ }
1010
+ __name(sendQQStory, "sendQQStory");
1011
+
970
1012
  // src/plugin/serverHandel/text.ts
971
1013
  var handelr_text = class extends handel_base {
972
1014
  static {
@@ -975,12 +1017,16 @@ var handelr_text = class extends handel_base {
975
1017
  async start(data) {
976
1018
  const content = new AnalysisMsg().parse(data);
977
1019
  const session = sessions_default.get(sessionKey(data.botId, data.userId, data.channelId));
978
- if ((data.commandHelp || data.transaction || data.pageNav) && canSendQQPage(session, data)) {
1020
+ const inlineCommands = data.line.some((item) => (item.type === 1 /* line */ || item.type === 2 /* error */) && hasInlineCommands(item.content));
1021
+ const story = hasStoryCodeBlock(data);
1022
+ if ((story || data.commandHelp || data.transaction || data.pageNav || inlineCommands) && canSendQQPage(session, data)) {
979
1023
  const sendStartedAt = this.timing ? performance.now() : 0;
980
1024
  try {
981
- if (data.commandHelp) await sendQQCommand(session, data);
1025
+ if (story) await sendQQStory(session, data);
1026
+ else if (data.commandHelp) await sendQQCommand(session, data);
982
1027
  else if (data.transaction) await sendQQTransaction(session, data);
983
- else await sendQQPage(session, data);
1028
+ else if (data.pageNav) await sendQQPage(session, data);
1029
+ else await sendQQCommand(session, data);
984
1030
  if (this.timing) {
985
1031
  this.timing.delivery = "qq-markdown";
986
1032
  this.timing.sendMs = performance.now() - sendStartedAt;
@@ -989,7 +1035,7 @@ var handelr_text = class extends handel_base {
989
1035
  } catch (error) {
990
1036
  this.ctx.logger.warn("QQ Markdown 发送失败,改用普通文字", error instanceof Error ? error.message : error);
991
1037
  }
992
- const commands = !data.commandHelp && !data.transaction && data.pageNav ? pageCommandLines(data.pageNav) : [];
1038
+ const commands = !story && !data.commandHelp && !data.transaction && data.pageNav ? pageCommandLines(data.pageNav) : [];
993
1039
  await this.send(data, commands.length ? `${content.trimEnd()}
994
1040
  ${commands.join("\n")}` : content);
995
1041
  return;
@@ -998,6 +1044,38 @@ ${commands.join("\n")}` : content);
998
1044
  }
999
1045
  };
1000
1046
 
1047
+ // src/plugin/serverHandel/image.ts
1048
+ var ImageHandler = class extends handel_base {
1049
+ static {
1050
+ __name(this, "ImageHandler");
1051
+ }
1052
+ async start(data) {
1053
+ let content = "";
1054
+ let rendered = false;
1055
+ const renderStartedAt = this.timing ? performance.now() : 0;
1056
+ try {
1057
+ if (!data.characterCard && !data.itemCard && !data.card && !data.leaderboard && !data.equipmentCard) throw new Error("缺少卡片数据");
1058
+ const image = await renderSkillCard(this.ctx, data.card, data.leaderboard, data.itemCard, data.characterCard, data.equipmentCard);
1059
+ if (this.timing) this.timing.imageBytes = image.length;
1060
+ content = import_koishi.h.image(image, "image/jpeg").toString();
1061
+ rendered = true;
1062
+ } catch (error) {
1063
+ this.ctx.logger.warn("卡片绘制失败,使用文字详情", error instanceof Error ? error.message : "unknown");
1064
+ } finally {
1065
+ if (this.timing) this.timing.renderMs = performance.now() - renderStartedAt;
1066
+ }
1067
+ if (!rendered) {
1068
+ const fallback = new handelr_text(data);
1069
+ fallback.set(this.ctx);
1070
+ if (this.timing) fallback.setTiming(this.timing);
1071
+ fallback.send = (message, text, type = "text") => this.send(message, text, type);
1072
+ await fallback.start(data);
1073
+ return;
1074
+ }
1075
+ await this.send(data, content, "image");
1076
+ }
1077
+ };
1078
+
1001
1079
  // src/plugin/messageQueue.ts
1002
1080
  var handlers = {
1003
1081
  ["text" /* text */]: handelr_text,
@@ -1062,7 +1140,7 @@ var import_tsrpc = require("tsrpc");
1062
1140
 
1063
1141
  // src/shared/bot/serviceProto.ts
1064
1142
  var serviceProto = {
1065
- "version": 13,
1143
+ "version": 14,
1066
1144
  "services": [
1067
1145
  {
1068
1146
  "id": 0,
@@ -1949,6 +2027,14 @@ var serviceProto = {
1949
2027
  "type": {
1950
2028
  "type": "String"
1951
2029
  }
2030
+ },
2031
+ {
2032
+ "id": 2,
2033
+ "name": "codeBlock",
2034
+ "type": {
2035
+ "type": "Boolean"
2036
+ },
2037
+ "optional": true
1952
2038
  }
1953
2039
  ]
1954
2040
  },
@@ -2280,10 +2366,6 @@ var usage = `
2280
2366
  踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
2281
2367
 
2282
2368
  无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
2283
-
2284
- ---
2285
-
2286
- 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
2287
2369
  `;
2288
2370
  var Config = import_koishi2.Schema.object({
2289
2371
  服务器地址: import_koishi2.Schema.string().default("ws://dew-v7-test:3021").description("DEW 游戏服的 WebSocket 地址;同机部署可填写 ws://127.0.0.1:3021")
@@ -1,5 +1,5 @@
1
1
  import type { Session } from 'koishi';
2
2
  import type { MsgMessage } from '../../shared/bot/MsgMessage';
3
- /** Keep help text intact while making its explicitly boxed commands clickable. */
3
+ /** Render command help and ordinary prose with clickable bracketed commands. */
4
4
  export declare function renderQQCommandMarkdown(data: MsgMessage, showGreeting: boolean): string;
5
5
  export declare function sendQQCommand(session: Session, data: MsgMessage): Promise<void>;
@@ -3,7 +3,12 @@ import { type PageNavData } from '../../shared/face';
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage';
4
4
  /** Escape server-provided lines before embedding them in native QQ Markdown. */
5
5
  export declare function escapeMarkdown(value: string): string;
6
+ /** Keep native QQ Markdown titles consistent across message types. */
7
+ export declare function renderQQTitle(content: string): string;
6
8
  export declare function inlineCommandUrl(command: string): string;
9
+ export declare function hasInlineCommands(content: string): boolean;
10
+ /** Escape surrounding prose while rendering boxed commands as inline links. */
11
+ export declare function renderInlineCommands(content: string): string;
7
12
  export declare function pageCommandLines(nav: PageNavData): string[];
8
13
  export declare function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): string;
9
14
  /** Returns false if this is not the matching QQ session or the adapter has no native API. */
@@ -0,0 +1,6 @@
1
+ import type { Session } from 'koishi';
2
+ import type { MsgMessage } from '../../shared/bot/MsgMessage';
3
+ export declare function hasStoryCodeBlock(data: Pick<MsgMessage, 'line'>): boolean;
4
+ /** Only marked story lines become code blocks; result summaries remain prose. */
5
+ export declare function renderQQStoryMarkdown(data: MsgMessage, showGreeting: boolean): string;
6
+ export declare function sendQQStory(session: Session, data: MsgMessage): Promise<void>;
@@ -101,6 +101,8 @@ export interface ITitleMessage {
101
101
  export interface ILineMessage {
102
102
  type: MessageType.line;
103
103
  content: string;
104
+ /** 原文长故事:支持的平台使用 Markdown 代码块,其余平台按普通文字显示。 */
105
+ codeBlock?: boolean;
104
106
  }
105
107
  /** 错误消息项 **/
106
108
  export interface IErrorMessage {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-dew-bot",
3
3
  "description": "基于《无限恐怖》小说改编的 RPG 机器人。踏入主神空间,与伙伴迎接轮回试炼,书写属于你的无限冒险。",
4
- "version": "1.0.251",
4
+ "version": "1.0.252",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
package/src/index.ts CHANGED
@@ -15,10 +15,6 @@ export const usage: string = `
15
15
  踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
16
16
 
17
17
  无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
18
-
19
- ---
20
-
21
- 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
22
18
  `
23
19
 
24
20
  export interface Config {
@@ -1,11 +1,11 @@
1
1
  import { h } from 'koishi'
2
2
  import { MsgMessage } from '../../shared/bot/MsgMessage'
3
- import { AnalysisMsg } from '../../shared/bot/analysisMsg'
4
3
  import { renderSkillCard } from '../skillCard'
5
4
  import handel_base from './handel_base'
5
+ import TextHandler from './text'
6
6
  export default class ImageHandler extends handel_base {
7
7
  async start(data: MsgMessage) {
8
- let content: string
8
+ let content = ''
9
9
  let rendered = false
10
10
  const renderStartedAt = this.timing ? performance.now() : 0
11
11
  try {
@@ -16,10 +16,17 @@ export default class ImageHandler extends handel_base {
16
16
  rendered = true
17
17
  } catch (error) {
18
18
  this.ctx.logger.warn('卡片绘制失败,使用文字详情', error instanceof Error ? error.message : 'unknown')
19
- content = new AnalysisMsg().parse(data)
20
19
  } finally {
21
20
  if (this.timing) this.timing.renderMs = performance.now() - renderStartedAt
22
21
  }
23
- await this.send(data, content, rendered ? 'image' : 'text')
22
+ if (!rendered) {
23
+ const fallback = new TextHandler(data)
24
+ fallback.set(this.ctx)
25
+ if (this.timing) fallback.setTiming(this.timing)
26
+ fallback.send = (message, text, type = 'text') => this.send(message, text, type)
27
+ await fallback.start(data)
28
+ return
29
+ }
30
+ await this.send(data, content, 'image')
24
31
  }
25
32
  }
@@ -1,27 +1,9 @@
1
1
  import type { Session } from 'koishi'
2
2
  import { MessageType } from '../../shared/face'
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
- import { escapeMarkdown, inlineCommandUrl, sendQQMarkdown } from './qqPageMessage'
4
+ import { escapeMarkdown, renderInlineCommands, renderQQTitle, sendQQMarkdown } from './qqPageMessage'
5
5
 
6
- function renderCommandLine(content: string, commandHelp: boolean): string {
7
- // Only explicit command-description rows enable links. Unknown input and
8
- // other prose remain display text, even when they contain square brackets.
9
- if (!commandHelp || !/^\s*\[[^\[\]\r\n]+\]/.test(content)) return escapeMarkdown(content)
10
- const parts: string[] = []
11
- let offset = 0
12
- for (const match of content.matchAll(/\[([^\[\]\r\n]+)\]/g)) {
13
- parts.push(escapeMarkdown(content.slice(offset, match.index)))
14
- const command = match[1].split(/[++]/, 1)[0].trim()
15
- parts.push(command
16
- ? `[${escapeMarkdown(match[1])}](${inlineCommandUrl(command)})`
17
- : escapeMarkdown(match[0]))
18
- offset = match.index! + match[0].length
19
- }
20
- parts.push(escapeMarkdown(content.slice(offset)))
21
- return parts.join('')
22
- }
23
-
24
- /** Keep help text intact while making its explicitly boxed commands clickable. */
6
+ /** Render command help and ordinary prose with clickable bracketed commands. */
25
7
  export function renderQQCommandMarkdown(data: MsgMessage, showGreeting: boolean): string {
26
8
  const sections: string[] = []
27
9
  const bodyLines: string[] = []
@@ -33,15 +15,15 @@ export function renderQQCommandMarkdown(data: MsgMessage, showGreeting: boolean)
33
15
  switch (item.type) {
34
16
  case MessageType.title:
35
17
  flushBody()
36
- sections.push(`**${escapeMarkdown(item.content)}**`)
18
+ sections.push(renderQQTitle(item.content))
37
19
  break
38
20
  case MessageType.line:
39
21
  if (!item.content.trim()) flushBody()
40
- else bodyLines.push(renderCommandLine(item.content, !!data.commandHelp))
22
+ else bodyLines.push(renderInlineCommands(item.content))
41
23
  break
42
24
  case MessageType.error:
43
25
  flushBody()
44
- sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`)
26
+ sections.push(`❌ 出错了!错误原因:${renderInlineCommands(item.content)},请反馈至联系开发者`)
45
27
  break
46
28
  }
47
29
  }
@@ -35,6 +35,11 @@ function compactDisplayText(value: string): string {
35
35
  .replace(/[ \t\u3000]+(?=[\p{Script=Han}\p{Extended_Pictographic}\p{Regional_Indicator}])/gu, '')
36
36
  }
37
37
 
38
+ /** Keep native QQ Markdown titles consistent across message types. */
39
+ export function renderQQTitle(content: string): string {
40
+ return `**${escapeMarkdown(compactDisplayText(content))}**\n\n***`
41
+ }
42
+
38
43
  function compactListLabel(value: string): string {
39
44
  const label = compactDisplayText(value)
40
45
  // The book icon already identifies skill books. Only shorten the displayed
@@ -52,6 +57,47 @@ export function inlineCommandUrl(command: string): string {
52
57
  return `mqqapi://aio/inlinecmd?command=${encoded}&enter=true&reply=false`
53
58
  }
54
59
 
60
+
61
+ type InlineCommandSpan = { start: number; end: number; label: string; command: string }
62
+
63
+ /** Bracketed commands may appear anywhere in prose, including escaped brackets. */
64
+ function inlineCommandSpans(content: string): InlineCommandSpan[] {
65
+ const spans: InlineCommandSpan[] = []
66
+ let depth = 0, start = 0, labelStart = 0
67
+ for (let index = 0; index < content.length; index++) {
68
+ if (content[index] === '[') {
69
+ if (depth === 0) {
70
+ start = index > 0 && content[index - 1] === '\\' ? index - 1 : index
71
+ labelStart = index + 1
72
+ }
73
+ depth++
74
+ } else if (content[index] === ']' && depth > 0 && --depth === 0) {
75
+ const label = content.slice(labelStart, index).replace(/\\$/, '')
76
+ if (!label.trim() || /[\[\]\r\n]/.test(label) || /^\d+$/.test(label.trim())) continue
77
+ const command = label.split(/[++]/, 1)[0].trim()
78
+ if (command) spans.push({ start, end: index + 1, label, command })
79
+ }
80
+ }
81
+ return spans
82
+ }
83
+
84
+ export function hasInlineCommands(content: string): boolean {
85
+ return inlineCommandSpans(content).length > 0
86
+ }
87
+
88
+ /** Escape surrounding prose while rendering boxed commands as inline links. */
89
+ export function renderInlineCommands(content: string): string {
90
+ const parts: string[] = []
91
+ let offset = 0
92
+ for (const span of inlineCommandSpans(content)) {
93
+ parts.push(escapeMarkdown(content.slice(offset, span.start)))
94
+ parts.push('[' + escapeMarkdown(span.label) + '](' + inlineCommandUrl(span.command) + ')')
95
+ offset = span.end
96
+ }
97
+ parts.push(escapeMarkdown(content.slice(offset)))
98
+ return parts.join('')
99
+ }
100
+
55
101
  export function pageCommandLines(nav: PageNavData): string[] {
56
102
  return [
57
103
  ...(nav.previousCommand ? [nav.previousCommand] : []),
@@ -65,11 +111,6 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): s
65
111
  const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/
66
112
  const pagePattern = /^第\s*\d+\s*\/\s*\d+\s*页(?:\s*[||]\s*共\s*(\d+)\s*([项条]))?$/
67
113
  const title = data.line.find(item => item.type === MessageType.title)
68
- const summaryLine = data.line.find(item => item.type === MessageType.line && pagePattern.test(item.content))
69
- const summary = summaryLine && pagePattern.exec(summaryLine.content)
70
- const listCount = data.line.filter(item => item.type === MessageType.line && listPattern.test(item.content)).length
71
- const count = summary?.[1] ?? (nav?.total === 1 && listCount > 0 ? String(listCount) : undefined)
72
- const countUnit = summary?.[2] ?? '项'
73
114
  const isLeaderboardDirectory = !!nav?.entries?.length
74
115
  && /^(?:🏆)?排行榜$/u.test(compactDisplayText(title?.content ?? ''))
75
116
  const sections: string[] = []
@@ -90,22 +131,18 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): s
90
131
  switch (item.type) {
91
132
  case MessageType.title:
92
133
  flush()
93
- if (isLeaderboardDirectory) {
94
- sections.push(`## ${item.content.includes('🏆') ? '' : '🏆'}${escapeMarkdown(compactDisplayText(item.content))}`)
95
- sections.push(`> 共${nav!.entries!.length}个榜单·点击榜名查看`)
96
- } else {
97
- sections.push(`**${escapeMarkdown(compactDisplayText(item.content))}${count !== undefined ? `·${count}${countUnit}` : ''}**`)
98
- }
134
+ sections.push(renderQQTitle(`${isLeaderboardDirectory && !item.content.includes('🏆') ? '🏆' : ''}${item.content}`))
99
135
  break
100
136
  case MessageType.line: {
101
137
  const listEntry = listPattern.exec(item.content)
102
138
  if (listEntry) {
103
139
  flushBody()
104
140
  const command = entryCommands.get(lineIndex)
105
- const label = escapeMarkdown(compactListLabel(listEntry[2]))
141
+ const content = compactListLabel(listEntry[2])
142
+ const label = escapeMarkdown(content)
106
143
  const name = command
107
144
  ? `[${label}](${inlineCommandUrl(command)})`
108
- : label
145
+ : renderInlineCommands(content)
109
146
  // Keep the numeric marker as Markdown syntax; escape only the item text.
110
147
  listLines.push(`${listEntry[1]}. ${name}`)
111
148
  break
@@ -125,19 +162,19 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): s
125
162
  const page = pagePattern.exec(item.content)
126
163
  if (page && nav) {
127
164
  flushBody()
128
- pageFooter = `**第${nav.current}/${nav.total}页${!title && page[1] ? `·共${page[1]}${page[2]}` : ''}**`
165
+ pageFooter = `**第${nav.current}/${nav.total}页**`
129
166
  hasPageSummary = true
130
167
  break
131
168
  }
132
169
  const command = entryCommands.get(lineIndex)
133
170
  bodyLines.push(command
134
- ? `[${escapeMarkdown(compactDisplayText(item.content))}](${inlineCommandUrl(command)})`
135
- : escapeMarkdown(item.content))
171
+ ? `[${escapeMarkdown(compactListLabel(item.content))}](${inlineCommandUrl(command)})`
172
+ : renderInlineCommands(item.content))
136
173
  break
137
174
  }
138
175
  case MessageType.error:
139
176
  flush()
140
- sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`)
177
+ sections.push(`❌ 出错了!错误原因:${renderInlineCommands(item.content)},请反馈至联系开发者`)
141
178
  break
142
179
  }
143
180
  }
@@ -154,10 +191,7 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): s
154
191
  const footer = pageFooter
155
192
  ? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join('·')
156
193
  : helpLink ? `更多操作:${helpLink}` : undefined
157
- if (footer) {
158
- if (isLeaderboardDirectory) sections.push('***')
159
- sections.push(footer)
160
- }
194
+ if (footer) sections.push(footer)
161
195
  return sections.join('\n\n') || ' '
162
196
  }
163
197
 
@@ -0,0 +1,56 @@
1
+ import type { Session } from 'koishi'
2
+ import { MessageType } from '../../shared/face'
3
+ import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
+ import { escapeMarkdown, renderInlineCommands, renderQQTitle, sendQQMarkdown } from './qqPageMessage'
5
+
6
+ export function hasStoryCodeBlock(data: Pick<MsgMessage, 'line'>): boolean {
7
+ return data.line.some(item => item.type === MessageType.line && item.codeBlock === true)
8
+ }
9
+
10
+ function fencedStory(content: string): string {
11
+ const story = content.replace(/\r\n?/g, '\n')
12
+ // A story may itself contain backticks. A longer fence keeps its entire text
13
+ // inside the same block without altering the source used by plain fallback.
14
+ const longestRun = Math.max(0, ...Array.from(story.matchAll(/`+/g), match => match[0].length))
15
+ const fence = '`'.repeat(Math.max(3, longestRun + 1))
16
+ return fence + '\n' + story + (story.endsWith('\n') ? '' : '\n') + fence
17
+ }
18
+
19
+ /** Only marked story lines become code blocks; result summaries remain prose. */
20
+ export function renderQQStoryMarkdown(data: MsgMessage, showGreeting: boolean): string {
21
+ const sections: string[] = []
22
+ const bodyLines: string[] = []
23
+ const flushBody = () => {
24
+ if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
25
+ }
26
+ if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
27
+ for (const item of data.line) {
28
+ switch (item.type) {
29
+ case MessageType.title:
30
+ flushBody()
31
+ sections.push(renderQQTitle(item.content))
32
+ break
33
+ case MessageType.line:
34
+ if (item.codeBlock) {
35
+ flushBody()
36
+ sections.push(fencedStory(item.content))
37
+ } else {
38
+ for (const line of item.content.replace(/\r\n?/g, '\n').split('\n')) {
39
+ if (!line.trim()) flushBody()
40
+ else bodyLines.push(renderInlineCommands(line))
41
+ }
42
+ }
43
+ break
44
+ case MessageType.error:
45
+ flushBody()
46
+ sections.push('❌ 出错了!错误原因:' + renderInlineCommands(item.content) + ',请反馈至联系开发者')
47
+ break
48
+ }
49
+ }
50
+ flushBody()
51
+ return sections.join('\n\n') || ' '
52
+ }
53
+
54
+ export async function sendQQStory(session: Session, data: MsgMessage): Promise<void> {
55
+ await sendQQMarkdown(session, renderQQStoryMarkdown(data, session.event?.channel?.type === 0))
56
+ }
@@ -1,7 +1,7 @@
1
1
  import type { Session } from 'koishi'
2
2
  import { MessageType } from '../../shared/face'
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
- import { escapeMarkdown, inlineCommandUrl, sendQQMarkdown } from './qqPageMessage'
4
+ import { escapeMarkdown, inlineCommandUrl, renderInlineCommands, renderQQTitle, sendQQMarkdown } from './qqPageMessage'
5
5
 
6
6
  /** Render only explicitly marked transactions; line content never enables this channel. */
7
7
  export function renderQQTransactionMarkdown(data: MsgMessage, showGreeting: boolean): string {
@@ -19,17 +19,16 @@ export function renderQQTransactionMarkdown(data: MsgMessage, showGreeting: bool
19
19
  for (const item of data.line) {
20
20
  if (item.type === MessageType.title) {
21
21
  flush()
22
- sections.push(`**${escapeMarkdown(item.content)}**`)
22
+ sections.push(renderQQTitle(item.content))
23
23
  continue
24
24
  }
25
25
  if (item.type === MessageType.error) {
26
26
  flush()
27
- sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`)
27
+ sections.push(`❌ 出错了!错误原因:${renderInlineCommands(item.content)},请反馈至联系开发者`)
28
28
  continue
29
29
  }
30
30
  if (item.type !== MessageType.line) continue
31
- // These are the common trade actions. Names, reasons and arbitrary bracketed
32
- // material text remain escaped display text rather than executable commands.
31
+ // Keep the common trade actions grouped; other bracketed commands render inline.
33
32
  const action = data.transaction && (item.content === '[是]确认交易'
34
33
  ? { command: '是', label: '确认交易' }
35
34
  : item.content === '[否]取消交易' ? { command: '否', label: '取消交易' } : undefined)
@@ -50,7 +49,7 @@ export function renderQQTransactionMarkdown(data: MsgMessage, showGreeting: bool
50
49
  } else {
51
50
  // Preserve the server's quoted amounts, names and descriptions verbatim
52
51
  // apart from Markdown escaping; rendering never recalculates payments.
53
- bodyLines.push(escapeMarkdown(item.content))
52
+ bodyLines.push(renderInlineCommands(item.content))
54
53
  }
55
54
  }
56
55
  flush()
@@ -1,21 +1,27 @@
1
+ import { MessageType } from '../../shared/face'
1
2
  import { AnalysisMsg } from '../../shared/bot/analysisMsg'
2
3
  import { MsgMessage } from '../../shared/bot/MsgMessage'
3
4
  import sessions, { sessionKey } from '../sessions'
4
5
  import handel_base from './handel_base'
5
- import { canSendQQPage, pageCommandLines, sendQQPage } from './qqPageMessage'
6
+ import { canSendQQPage, hasInlineCommands, pageCommandLines, sendQQPage } from './qqPageMessage'
6
7
  import { sendQQTransaction } from './qqTransactionMessage'
7
8
  import { sendQQCommand } from './qqCommandMessage'
9
+ import { hasStoryCodeBlock, sendQQStory } from './qqStoryMessage'
8
10
 
9
11
  export default class handelr_text extends handel_base {
10
12
  async start(data: MsgMessage) {
11
13
  const content = new AnalysisMsg().parse(data)
12
14
  const session = sessions.get(sessionKey(data.botId, data.userId, data.channelId))
13
- if ((data.commandHelp || data.transaction || data.pageNav) && canSendQQPage(session, data)) {
15
+ const inlineCommands = data.line.some(item => (item.type === MessageType.line || item.type === MessageType.error) && hasInlineCommands(item.content))
16
+ const story = hasStoryCodeBlock(data)
17
+ if ((story || data.commandHelp || data.transaction || data.pageNav || inlineCommands) && canSendQQPage(session, data)) {
14
18
  const sendStartedAt = this.timing ? performance.now() : 0
15
19
  try {
16
- if (data.commandHelp) await sendQQCommand(session, data)
20
+ if (story) await sendQQStory(session, data)
21
+ else if (data.commandHelp) await sendQQCommand(session, data)
17
22
  else if (data.transaction) await sendQQTransaction(session, data)
18
- else await sendQQPage(session, data)
23
+ else if (data.pageNav) await sendQQPage(session, data)
24
+ else await sendQQCommand(session, data)
19
25
  if (this.timing) {
20
26
  this.timing.delivery = 'qq-markdown'
21
27
  this.timing.sendMs = performance.now() - sendStartedAt
@@ -24,7 +30,7 @@ export default class handelr_text extends handel_base {
24
30
  } catch (error) {
25
31
  this.ctx.logger.warn('QQ Markdown 发送失败,改用普通文字', error instanceof Error ? error.message : error)
26
32
  }
27
- const commands = !data.commandHelp && !data.transaction && data.pageNav ? pageCommandLines(data.pageNav) : []
33
+ const commands = !story && !data.commandHelp && !data.transaction && data.pageNav ? pageCommandLines(data.pageNav) : []
28
34
  await this.send(data, commands.length ? `${content.trimEnd()}\n${commands.join('\n')}` : content)
29
35
  return
30
36
  }
@@ -20,7 +20,7 @@ export interface ServiceType {
20
20
  }
21
21
 
22
22
  export const serviceProto: ServiceProto<ServiceType> = {
23
- "version": 13,
23
+ "version": 14,
24
24
  "services": [
25
25
  {
26
26
  "id": 0,
@@ -907,6 +907,14 @@ export const serviceProto: ServiceProto<ServiceType> = {
907
907
  "type": {
908
908
  "type": "String"
909
909
  }
910
+ },
911
+ {
912
+ "id": 2,
913
+ "name": "codeBlock",
914
+ "type": {
915
+ "type": "Boolean"
916
+ },
917
+ "optional": true
910
918
  }
911
919
  ]
912
920
  },
@@ -73,6 +73,8 @@ export interface ITitleMessage {
73
73
  export interface ILineMessage {
74
74
  type: MessageType.line;
75
75
  content: string;
76
+ /** 原文长故事:支持的平台使用 Markdown 代码块,其余平台按普通文字显示。 */
77
+ codeBlock?: boolean;
76
78
  }
77
79
 
78
80
  /** 错误消息项 **/