koishi-plugin-dew-bot 1.0.252 → 1.0.253

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.js CHANGED
@@ -689,7 +689,7 @@ var AnalysisMsg = class {
689
689
  }
690
690
  };
691
691
 
692
- // src/plugin/serverHandel/qqPageMessage.ts
692
+ // src/plugin/serverHandel/qqMarkdownHelpers.ts
693
693
  function escapeMarkdown(value) {
694
694
  return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, "\\$&");
695
695
  }
@@ -704,6 +704,62 @@ function renderQQTitle(content) {
704
704
  ***`;
705
705
  }
706
706
  __name(renderQQTitle, "renderQQTitle");
707
+
708
+ // src/plugin/serverHandel/qqRewardMessage.ts
709
+ function hasRewardLines(data) {
710
+ return data.line.some((item) => item.type === 1 /* line */ && item.reward === true);
711
+ }
712
+ __name(hasRewardLines, "hasRewardLines");
713
+ function isRewardMessage(data) {
714
+ return data.reward === true || hasRewardLines(data);
715
+ }
716
+ __name(isRewardMessage, "isRewardMessage");
717
+ function renderRewardTitle(content) {
718
+ return `**${escapeMarkdown(compactDisplayText(content))}**`;
719
+ }
720
+ __name(renderRewardTitle, "renderRewardTitle");
721
+ function renderRewardLine(content) {
722
+ return content.replace(/\r\n?/g, "\n").split("\n").filter((line) => line.trim()).map((line) => {
723
+ const text = line.trim();
724
+ const quantity = /([++\-−×xX]\s*\d+(?:\.\d+)?)$/.exec(text);
725
+ const formatted = quantity ? escapeMarkdown(text.slice(0, quantity.index)) + `**${escapeMarkdown(quantity[1])}**` : escapeMarkdown(text);
726
+ return "> " + formatted;
727
+ }).join(" \n");
728
+ }
729
+ __name(renderRewardLine, "renderRewardLine");
730
+ function renderQQRewardMarkdown(data, showGreeting) {
731
+ const sections = [];
732
+ const bodyLines = [];
733
+ const rewardLines = [];
734
+ const flushBody = /* @__PURE__ */ __name(() => {
735
+ if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
736
+ }, "flushBody");
737
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
738
+ for (const item of data.line) {
739
+ if (item.type === 1 /* line */ && item.reward) {
740
+ const reward = renderRewardLine(item.content);
741
+ if (reward) rewardLines.push(reward);
742
+ continue;
743
+ }
744
+ if (item.type === 0 /* title */) {
745
+ flushBody();
746
+ bodyLines.push(renderRewardTitle(item.content));
747
+ } else if (item.type === 1 /* line */) {
748
+ for (const line of item.content.replace(/\r\n?/g, "\n").split("\n")) {
749
+ if (!line.trim()) flushBody();
750
+ else bodyLines.push(escapeMarkdown(line));
751
+ }
752
+ } else if (item.type === 2 /* error */) {
753
+ bodyLines.push(`❌ ${escapeMarkdown(item.content)}`);
754
+ }
755
+ }
756
+ flushBody();
757
+ if (rewardLines.length) sections.push(rewardLines.join(" \n"));
758
+ return sections.join("\n\n") || " ";
759
+ }
760
+ __name(renderQQRewardMarkdown, "renderQQRewardMarkdown");
761
+
762
+ // src/plugin/serverHandel/qqPageMessage.ts
707
763
  function compactListLabel(value) {
708
764
  const label = compactDisplayText(value);
709
765
  return /[📖📚]/u.test(label) ? label.replace(/技能书(?=$|×\d+(?:\.\d+)?(?:||$)||)/g, "") : label;
@@ -758,6 +814,7 @@ function pageCommandLines(nav) {
758
814
  }
759
815
  __name(pageCommandLines, "pageCommandLines");
760
816
  function renderQQPageMarkdown(data, showGreeting) {
817
+ const rewardMessage = isRewardMessage(data);
761
818
  const nav = data.pageNav;
762
819
  const entryCommands = new Map(nav?.entries?.map((entry) => [entry.lineIndex, entry.viewCommand]) ?? []);
763
820
  const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/;
@@ -767,27 +824,42 @@ function renderQQPageMarkdown(data, showGreeting) {
767
824
  const sections = [];
768
825
  const bodyLines = [];
769
826
  const listLines = [];
827
+ const rewardLines = [];
770
828
  const flushBody = /* @__PURE__ */ __name(() => {
771
829
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
772
830
  }, "flushBody");
773
831
  const flushList = /* @__PURE__ */ __name(() => {
774
832
  if (listLines.length) sections.push(listLines.splice(0).join("\n"));
775
833
  }, "flushList");
834
+ const flushRewards = /* @__PURE__ */ __name(() => {
835
+ if (rewardLines.length) sections.push(rewardLines.splice(0).join(" \n"));
836
+ }, "flushRewards");
776
837
  const flush = /* @__PURE__ */ __name(() => {
777
838
  flushList();
778
839
  flushBody();
840
+ flushRewards();
779
841
  }, "flush");
780
842
  let hasPageSummary = false;
781
843
  let hasHelp = false;
782
844
  let pageFooter;
783
- if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
845
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
784
846
  for (const [lineIndex, item] of data.line.entries()) {
785
847
  switch (item.type) {
786
848
  case 0 /* title */:
787
849
  flush();
788
- sections.push(renderQQTitle(`${isLeaderboardDirectory && !item.content.includes("🏆") ? "🏆" : ""}${item.content}`));
850
+ const titleText = `${isLeaderboardDirectory && !item.content.includes("🏆") ? "🏆" : ""}${item.content}`;
851
+ if (rewardMessage) bodyLines.push(renderRewardTitle(titleText));
852
+ else sections.push(renderQQTitle(titleText));
789
853
  break;
790
854
  case 1 /* line */: {
855
+ if (item.reward) {
856
+ flushList();
857
+ flushBody();
858
+ const reward = renderRewardLine(item.content);
859
+ if (reward) rewardLines.push(reward);
860
+ break;
861
+ }
862
+ flushRewards();
791
863
  const listEntry = listPattern.exec(item.content);
792
864
  if (listEntry) {
793
865
  flushBody();
@@ -871,24 +943,31 @@ __name(sendQQMarkdown, "sendQQMarkdown");
871
943
 
872
944
  // src/plugin/serverHandel/qqTransactionMessage.ts
873
945
  function renderQQTransactionMarkdown(data, showGreeting) {
946
+ const rewardMessage = isRewardMessage(data);
874
947
  const sections = [];
875
948
  const bodyLines = [];
876
949
  const actions = [];
950
+ const rewardLines = [];
877
951
  const flushBody = /* @__PURE__ */ __name(() => {
878
952
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
879
953
  }, "flushBody");
880
954
  const flushActions = /* @__PURE__ */ __name(() => {
881
955
  if (actions.length) sections.push(actions.splice(0).join(" · "));
882
956
  }, "flushActions");
957
+ const flushRewards = /* @__PURE__ */ __name(() => {
958
+ if (rewardLines.length) sections.push(rewardLines.splice(0).join(" \n"));
959
+ }, "flushRewards");
883
960
  const flush = /* @__PURE__ */ __name(() => {
884
961
  flushBody();
885
962
  flushActions();
963
+ flushRewards();
886
964
  }, "flush");
887
- if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
965
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
888
966
  for (const item of data.line) {
889
967
  if (item.type === 0 /* title */) {
890
968
  flush();
891
- sections.push(renderQQTitle(item.content));
969
+ if (rewardMessage) bodyLines.push(renderRewardTitle(item.content));
970
+ else sections.push(renderQQTitle(item.content));
892
971
  continue;
893
972
  }
894
973
  if (item.type === 2 /* error */) {
@@ -897,6 +976,14 @@ function renderQQTransactionMarkdown(data, showGreeting) {
897
976
  continue;
898
977
  }
899
978
  if (item.type !== 1 /* line */) continue;
979
+ if (item.reward) {
980
+ flushBody();
981
+ flushActions();
982
+ const reward = renderRewardLine(item.content);
983
+ if (reward) rewardLines.push(reward);
984
+ continue;
985
+ }
986
+ flushRewards();
900
987
  const action = data.transaction && (item.content === "[是]确认交易" ? { command: "是", label: "确认交易" } : item.content === "[否]取消交易" ? { command: "否", label: "取消交易" } : void 0);
901
988
  if (action) {
902
989
  flushBody();
@@ -927,29 +1014,48 @@ __name(sendQQTransaction, "sendQQTransaction");
927
1014
 
928
1015
  // src/plugin/serverHandel/qqCommandMessage.ts
929
1016
  function renderQQCommandMarkdown(data, showGreeting) {
1017
+ const rewardMessage = isRewardMessage(data);
1018
+ if (!data.commandHelp && !data.pageNav && data.line.some((item) => item.type === 1 /* line */ && !item.reward && /^\[\d+\]\s*[^\r\n]+$/.test(item.content))) {
1019
+ return renderQQPageMarkdown(data, showGreeting);
1020
+ }
930
1021
  const sections = [];
931
1022
  const bodyLines = [];
1023
+ const rewardLines = [];
932
1024
  const flushBody = /* @__PURE__ */ __name(() => {
933
1025
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
934
1026
  }, "flushBody");
935
- if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
1027
+ const flushRewards = /* @__PURE__ */ __name(() => {
1028
+ if (rewardLines.length) sections.push(rewardLines.splice(0).join(" \n"));
1029
+ }, "flushRewards");
1030
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
936
1031
  for (const item of data.line) {
937
1032
  switch (item.type) {
938
1033
  case 0 /* title */:
939
1034
  flushBody();
940
- sections.push(renderQQTitle(item.content));
1035
+ flushRewards();
1036
+ if (rewardMessage) bodyLines.push(renderRewardTitle(item.content));
1037
+ else sections.push(renderQQTitle(item.content));
941
1038
  break;
942
1039
  case 1 /* line */:
1040
+ if (item.reward) {
1041
+ flushBody();
1042
+ const reward = renderRewardLine(item.content);
1043
+ if (reward) rewardLines.push(reward);
1044
+ break;
1045
+ }
1046
+ flushRewards();
943
1047
  if (!item.content.trim()) flushBody();
944
1048
  else bodyLines.push(renderInlineCommands(item.content));
945
1049
  break;
946
1050
  case 2 /* error */:
947
1051
  flushBody();
1052
+ flushRewards();
948
1053
  sections.push(`❌ 出错了!错误原因:${renderInlineCommands(item.content)},请反馈至联系开发者`);
949
1054
  break;
950
1055
  }
951
1056
  }
952
1057
  flushBody();
1058
+ flushRewards();
953
1059
  return sections.join("\n\n") || " ";
954
1060
  }
955
1061
  __name(renderQQCommandMarkdown, "renderQQCommandMarkdown");
@@ -971,36 +1077,52 @@ function fencedStory(content) {
971
1077
  }
972
1078
  __name(fencedStory, "fencedStory");
973
1079
  function renderQQStoryMarkdown(data, showGreeting) {
1080
+ const rewardMessage = isRewardMessage(data);
974
1081
  const sections = [];
975
1082
  const bodyLines = [];
1083
+ const rewardLines = [];
976
1084
  const flushBody = /* @__PURE__ */ __name(() => {
977
1085
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
978
1086
  }, "flushBody");
979
- if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
1087
+ const flushRewards = /* @__PURE__ */ __name(() => {
1088
+ if (rewardLines.length) sections.push(rewardLines.splice(0).join(" \n"));
1089
+ }, "flushRewards");
1090
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
980
1091
  for (const item of data.line) {
981
1092
  switch (item.type) {
982
1093
  case 0 /* title */:
983
1094
  flushBody();
984
- sections.push(renderQQTitle(item.content));
1095
+ if (!data.reward) flushRewards();
1096
+ if (rewardMessage) bodyLines.push(renderRewardTitle(item.content));
1097
+ else sections.push(renderQQTitle(item.content));
985
1098
  break;
986
1099
  case 1 /* line */:
1100
+ if (item.reward) {
1101
+ if (!data.reward) flushBody();
1102
+ const reward = renderRewardLine(item.content);
1103
+ if (reward) rewardLines.push(reward);
1104
+ break;
1105
+ }
1106
+ if (!data.reward) flushRewards();
987
1107
  if (item.codeBlock) {
988
1108
  flushBody();
989
1109
  sections.push(fencedStory(item.content));
990
1110
  } else {
991
1111
  for (const line of item.content.replace(/\r\n?/g, "\n").split("\n")) {
992
1112
  if (!line.trim()) flushBody();
993
- else bodyLines.push(renderInlineCommands(line));
1113
+ else bodyLines.push(data.reward ? escapeMarkdown(line) : renderInlineCommands(line));
994
1114
  }
995
1115
  }
996
1116
  break;
997
1117
  case 2 /* error */:
998
1118
  flushBody();
999
- sections.push("❌ 出错了!错误原因:" + renderInlineCommands(item.content) + ",请反馈至联系开发者");
1119
+ if (!data.reward) flushRewards();
1120
+ sections.push("❌ 出错了!错误原因:" + (data.reward ? escapeMarkdown(item.content) : renderInlineCommands(item.content)) + ",请反馈至联系开发者");
1000
1121
  break;
1001
1122
  }
1002
1123
  }
1003
1124
  flushBody();
1125
+ flushRewards();
1004
1126
  return sections.join("\n\n") || " ";
1005
1127
  }
1006
1128
  __name(renderQQStoryMarkdown, "renderQQStoryMarkdown");
@@ -1019,13 +1141,15 @@ var handelr_text = class extends handel_base {
1019
1141
  const session = sessions_default.get(sessionKey(data.botId, data.userId, data.channelId));
1020
1142
  const inlineCommands = data.line.some((item) => (item.type === 1 /* line */ || item.type === 2 /* error */) && hasInlineCommands(item.content));
1021
1143
  const story = hasStoryCodeBlock(data);
1022
- if ((story || data.commandHelp || data.transaction || data.pageNav || inlineCommands) && canSendQQPage(session, data)) {
1144
+ const reward = isRewardMessage(data);
1145
+ if ((story || data.commandHelp || data.transaction || data.pageNav || inlineCommands || reward) && canSendQQPage(session, data)) {
1023
1146
  const sendStartedAt = this.timing ? performance.now() : 0;
1024
1147
  try {
1025
1148
  if (story) await sendQQStory(session, data);
1026
1149
  else if (data.commandHelp) await sendQQCommand(session, data);
1027
1150
  else if (data.transaction) await sendQQTransaction(session, data);
1028
1151
  else if (data.pageNav) await sendQQPage(session, data);
1152
+ else if (reward) await sendQQMarkdown(session, renderQQRewardMarkdown(data, session.event?.channel?.type === 0));
1029
1153
  else await sendQQCommand(session, data);
1030
1154
  if (this.timing) {
1031
1155
  this.timing.delivery = "qq-markdown";
@@ -1140,7 +1264,7 @@ var import_tsrpc = require("tsrpc");
1140
1264
 
1141
1265
  // src/shared/bot/serviceProto.ts
1142
1266
  var serviceProto = {
1143
- "version": 14,
1267
+ "version": 15,
1144
1268
  "services": [
1145
1269
  {
1146
1270
  "id": 0,
@@ -1291,6 +1415,14 @@ var serviceProto = {
1291
1415
  },
1292
1416
  "optional": true
1293
1417
  },
1418
+ {
1419
+ "id": 10,
1420
+ "name": "reward",
1421
+ "type": {
1422
+ "type": "Boolean"
1423
+ },
1424
+ "optional": true
1425
+ },
1294
1426
  {
1295
1427
  "id": 1,
1296
1428
  "name": "line",
@@ -2035,6 +2167,14 @@ var serviceProto = {
2035
2167
  "type": "Boolean"
2036
2168
  },
2037
2169
  "optional": true
2170
+ },
2171
+ {
2172
+ "id": 3,
2173
+ "name": "reward",
2174
+ "type": {
2175
+ "type": "Boolean"
2176
+ },
2177
+ "optional": true
2038
2178
  }
2039
2179
  ]
2040
2180
  },
@@ -0,0 +1,6 @@
1
+ /** Escape server-provided text before embedding it in native QQ Markdown. */
2
+ export declare function escapeMarkdown(value: string): string;
3
+ /** Compact display padding while retaining spaces between English words. */
4
+ export declare function compactDisplayText(value: string): string;
5
+ /** Keep existing menu, help and transaction titles consistent. */
6
+ export declare function renderQQTitle(content: string): string;
@@ -1,10 +1,7 @@
1
1
  import type { Session } from 'koishi';
2
2
  import { type PageNavData } from '../../shared/face';
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage';
4
- /** Escape server-provided lines before embedding them in native QQ Markdown. */
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;
4
+ export { escapeMarkdown, renderQQTitle } from './qqMarkdownHelpers';
8
5
  export declare function inlineCommandUrl(command: string): string;
9
6
  export declare function hasInlineCommands(content: string): boolean;
10
7
  /** Escape surrounding prose while rendering boxed commands as inline links. */
@@ -0,0 +1,9 @@
1
+ import type { MsgMessage } from '../../shared/bot/MsgMessage';
2
+ export declare function hasRewardLines(data: Pick<MsgMessage, 'line'>): boolean;
3
+ /** The message marker also covers empty rewards and attachments already claimed. */
4
+ export declare function isRewardMessage(data: Pick<MsgMessage, 'line' | 'reward'>): boolean;
5
+ export declare function renderRewardTitle(content: string): string;
6
+ /** Reward names remain literal text; only the final signed quantity is emphasized. */
7
+ export declare function renderRewardLine(content: string): string;
8
+ /** Compact result card: title and description, followed by one continuous reward quote. */
9
+ export declare function renderQQRewardMarkdown(data: MsgMessage, showGreeting: boolean): string;
@@ -103,6 +103,8 @@ export interface ILineMessage {
103
103
  content: string;
104
104
  /** 原文长故事:支持的平台使用 Markdown 代码块,其余平台按普通文字显示。 */
105
105
  codeBlock?: boolean;
106
+ /** 奖励明细;支持的平台合并为引用区,普通文字仍使用 content。 */
107
+ reward?: boolean;
106
108
  }
107
109
  /** 错误消息项 **/
108
110
  export interface IErrorMessage {
@@ -136,6 +138,8 @@ export interface IMessage {
136
138
  transaction?: boolean;
137
139
  /** 指令系统与指令提示;客户端可用 Markdown 展示可点击的方框指令。 */
138
140
  commandHelp?: boolean;
141
+ /** 奖励结果卡:标题、描述、奖励;空奖励时也保留同一种布局。 */
142
+ reward?: boolean;
139
143
  /** 消息项列表 **/
140
144
  line: IMessageItem[];
141
145
  }
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.252",
4
+ "version": "1.0.253",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -1,33 +1,56 @@
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, renderInlineCommands, renderQQTitle, sendQQMarkdown } from './qqPageMessage'
4
+ import { escapeMarkdown, renderInlineCommands, renderQQPageMarkdown, renderQQTitle, sendQQMarkdown } from './qqPageMessage'
5
+ import { isRewardMessage, renderRewardLine, renderRewardTitle } from './qqRewardMessage'
5
6
 
6
7
  /** Render command help and ordinary prose with clickable bracketed commands. */
7
8
  export function renderQQCommandMarkdown(data: MsgMessage, showGreeting: boolean): string {
9
+ const rewardMessage = isRewardMessage(data)
10
+ // Menus without pagination still need native list markers. Escaped [n]
11
+ // markers can become separate centered blocks in QQ's Markdown renderer.
12
+ if (!data.commandHelp && !data.pageNav && data.line.some(item =>
13
+ item.type === MessageType.line && !item.reward && /^\[\d+\]\s*[^\r\n]+$/.test(item.content))) {
14
+ return renderQQPageMarkdown(data, showGreeting)
15
+ }
8
16
  const sections: string[] = []
9
17
  const bodyLines: string[] = []
18
+ const rewardLines: string[] = []
10
19
  const flushBody = () => {
11
20
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
12
21
  }
13
- if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
22
+ const flushRewards = () => {
23
+ if (rewardLines.length) sections.push(rewardLines.splice(0).join(' \n'))
24
+ }
25
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
14
26
  for (const item of data.line) {
15
27
  switch (item.type) {
16
28
  case MessageType.title:
17
29
  flushBody()
18
- sections.push(renderQQTitle(item.content))
30
+ flushRewards()
31
+ if (rewardMessage) bodyLines.push(renderRewardTitle(item.content))
32
+ else sections.push(renderQQTitle(item.content))
19
33
  break
20
34
  case MessageType.line:
35
+ if (item.reward) {
36
+ flushBody()
37
+ const reward = renderRewardLine(item.content)
38
+ if (reward) rewardLines.push(reward)
39
+ break
40
+ }
41
+ flushRewards()
21
42
  if (!item.content.trim()) flushBody()
22
43
  else bodyLines.push(renderInlineCommands(item.content))
23
44
  break
24
45
  case MessageType.error:
25
46
  flushBody()
47
+ flushRewards()
26
48
  sections.push(`❌ 出错了!错误原因:${renderInlineCommands(item.content)},请反馈至联系开发者`)
27
49
  break
28
50
  }
29
51
  }
30
52
  flushBody()
53
+ flushRewards()
31
54
  return sections.join('\n\n') || ' '
32
55
  }
33
56
 
@@ -0,0 +1,17 @@
1
+ /** Escape server-provided text before embedding it in native QQ Markdown. */
2
+ export function escapeMarkdown(value: string): string {
3
+ return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, '\\$&')
4
+ }
5
+
6
+ /** Compact display padding while retaining spaces between English words. */
7
+ export function compactDisplayText(value: string): string {
8
+ return value.trim()
9
+ .replace(/[ \t\u3000]*([·×+|])[ \t\u3000]*/g, '$1')
10
+ .replace(/([\p{Script=Han}\p{Extended_Pictographic}\p{Emoji_Modifier}\p{Regional_Indicator}\uFE0F])[ \t\u3000]+/gu, '$1')
11
+ .replace(/[ \t\u3000]+(?=[\p{Script=Han}\p{Extended_Pictographic}\p{Regional_Indicator}])/gu, '')
12
+ }
13
+
14
+ /** Keep existing menu, help and transaction titles consistent. */
15
+ export function renderQQTitle(content: string): string {
16
+ return `**${escapeMarkdown(compactDisplayText(content))}**\n\n***`
17
+ }
@@ -1,6 +1,10 @@
1
1
  import type { Session } from 'koishi'
2
2
  import { MessageType, type PageNavData } from '../../shared/face'
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
+ import { compactDisplayText, escapeMarkdown, renderQQTitle } from './qqMarkdownHelpers'
5
+ import { isRewardMessage, renderRewardLine, renderRewardTitle } from './qqRewardMessage'
6
+
7
+ export { escapeMarkdown, renderQQTitle } from './qqMarkdownHelpers'
4
8
 
5
9
  type QQRequest = {
6
10
  content: string
@@ -22,24 +26,6 @@ type QQResponse = {
22
26
  data?: { message_audit?: { audit_id?: string } }
23
27
  }
24
28
 
25
- /** Escape server-provided lines before embedding them in native QQ Markdown. */
26
- export function escapeMarkdown(value: string): string {
27
- return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, '\\$&')
28
- }
29
-
30
- /** Compact display padding while retaining spaces between English words. */
31
- function compactDisplayText(value: string): string {
32
- return value.trim()
33
- .replace(/[ \t\u3000]*([·×+|])[ \t\u3000]*/g, '$1')
34
- .replace(/([\p{Script=Han}\p{Extended_Pictographic}\p{Emoji_Modifier}\p{Regional_Indicator}\uFE0F])[ \t\u3000]+/gu, '$1')
35
- .replace(/[ \t\u3000]+(?=[\p{Script=Han}\p{Extended_Pictographic}\p{Regional_Indicator}])/gu, '')
36
- }
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
-
43
29
  function compactListLabel(value: string): string {
44
30
  const label = compactDisplayText(value)
45
31
  // The book icon already identifies skill books. Only shorten the displayed
@@ -106,6 +92,7 @@ export function pageCommandLines(nav: PageNavData): string[] {
106
92
  }
107
93
 
108
94
  export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): string {
95
+ const rewardMessage = isRewardMessage(data)
109
96
  const nav = data.pageNav
110
97
  const entryCommands = new Map(nav?.entries?.map(entry => [entry.lineIndex, entry.viewCommand]) ?? [])
111
98
  const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/
@@ -116,24 +103,38 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): s
116
103
  const sections: string[] = []
117
104
  const bodyLines: string[] = []
118
105
  const listLines: string[] = []
106
+ const rewardLines: string[] = []
119
107
  const flushBody = () => {
120
108
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
121
109
  }
122
110
  const flushList = () => {
123
111
  if (listLines.length) sections.push(listLines.splice(0).join('\n'))
124
112
  }
125
- const flush = () => { flushList(); flushBody() }
113
+ const flushRewards = () => {
114
+ if (rewardLines.length) sections.push(rewardLines.splice(0).join(' \n'))
115
+ }
116
+ const flush = () => { flushList(); flushBody(); flushRewards() }
126
117
  let hasPageSummary = false
127
118
  let hasHelp = false
128
119
  let pageFooter: string | undefined
129
- if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
120
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
130
121
  for (const [lineIndex, item] of data.line.entries()) {
131
122
  switch (item.type) {
132
123
  case MessageType.title:
133
124
  flush()
134
- sections.push(renderQQTitle(`${isLeaderboardDirectory && !item.content.includes('🏆') ? '🏆' : ''}${item.content}`))
125
+ const titleText = `${isLeaderboardDirectory && !item.content.includes('🏆') ? '🏆' : ''}${item.content}`
126
+ if (rewardMessage) bodyLines.push(renderRewardTitle(titleText))
127
+ else sections.push(renderQQTitle(titleText))
135
128
  break
136
129
  case MessageType.line: {
130
+ if (item.reward) {
131
+ flushList()
132
+ flushBody()
133
+ const reward = renderRewardLine(item.content)
134
+ if (reward) rewardLines.push(reward)
135
+ break
136
+ }
137
+ flushRewards()
137
138
  const listEntry = listPattern.exec(item.content)
138
139
  if (listEntry) {
139
140
  flushBody()
@@ -0,0 +1,62 @@
1
+ import { MessageType } from '../../shared/face'
2
+ import type { MsgMessage } from '../../shared/bot/MsgMessage'
3
+ import { compactDisplayText, escapeMarkdown } from './qqMarkdownHelpers'
4
+
5
+ export function hasRewardLines(data: Pick<MsgMessage, 'line'>): boolean {
6
+ return data.line.some(item => item.type === MessageType.line && item.reward === true)
7
+ }
8
+
9
+ /** The message marker also covers empty rewards and attachments already claimed. */
10
+ export function isRewardMessage(data: Pick<MsgMessage, 'line' | 'reward'>): boolean {
11
+ return data.reward === true || hasRewardLines(data)
12
+ }
13
+
14
+ export function renderRewardTitle(content: string): string {
15
+ return `**${escapeMarkdown(compactDisplayText(content))}**`
16
+ }
17
+
18
+ /** Reward names remain literal text; only the final signed quantity is emphasized. */
19
+ export function renderRewardLine(content: string): string {
20
+ return content.replace(/\r\n?/g, '\n').split('\n').filter(line => line.trim()).map(line => {
21
+ const text = line.trim()
22
+ const quantity = /([++\-−×xX]\s*\d+(?:\.\d+)?)$/.exec(text)
23
+ const formatted = quantity
24
+ ? escapeMarkdown(text.slice(0, quantity.index)) + `**${escapeMarkdown(quantity[1])}**`
25
+ : escapeMarkdown(text)
26
+ return '> ' + formatted
27
+ }).join(' \n')
28
+ }
29
+
30
+ /** Compact result card: title and description, followed by one continuous reward quote. */
31
+ export function renderQQRewardMarkdown(data: MsgMessage, showGreeting: boolean): string {
32
+ const sections: string[] = []
33
+ const bodyLines: string[] = []
34
+ const rewardLines: string[] = []
35
+ const flushBody = () => {
36
+ if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
37
+ }
38
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
39
+ for (const item of data.line) {
40
+ if (item.type === MessageType.line && item.reward) {
41
+ const reward = renderRewardLine(item.content)
42
+ if (reward) rewardLines.push(reward)
43
+ continue
44
+ }
45
+ if (item.type === MessageType.title) {
46
+ flushBody()
47
+ bodyLines.push(renderRewardTitle(item.content))
48
+ } else if (item.type === MessageType.line) {
49
+ for (const line of item.content.replace(/\r\n?/g, '\n').split('\n')) {
50
+ if (!line.trim()) flushBody()
51
+ else bodyLines.push(escapeMarkdown(line))
52
+ }
53
+ } else if (item.type === MessageType.error) {
54
+ bodyLines.push(`❌ ${escapeMarkdown(item.content)}`)
55
+ }
56
+ }
57
+ flushBody()
58
+ // Runtime notices and level-up summaries may be appended after the source
59
+ // reward rows. Keep every description above one final reward quote.
60
+ if (rewardLines.length) sections.push(rewardLines.join(' \n'))
61
+ return sections.join('\n\n') || ' '
62
+ }
@@ -2,6 +2,7 @@ import type { Session } from 'koishi'
2
2
  import { MessageType } from '../../shared/face'
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
4
  import { escapeMarkdown, renderInlineCommands, renderQQTitle, sendQQMarkdown } from './qqPageMessage'
5
+ import { isRewardMessage, renderRewardLine, renderRewardTitle } from './qqRewardMessage'
5
6
 
6
7
  export function hasStoryCodeBlock(data: Pick<MsgMessage, 'line'>): boolean {
7
8
  return data.line.some(item => item.type === MessageType.line && item.codeBlock === true)
@@ -18,36 +19,52 @@ function fencedStory(content: string): string {
18
19
 
19
20
  /** Only marked story lines become code blocks; result summaries remain prose. */
20
21
  export function renderQQStoryMarkdown(data: MsgMessage, showGreeting: boolean): string {
22
+ const rewardMessage = isRewardMessage(data)
21
23
  const sections: string[] = []
22
24
  const bodyLines: string[] = []
25
+ const rewardLines: string[] = []
23
26
  const flushBody = () => {
24
27
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
25
28
  }
26
- if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
29
+ const flushRewards = () => {
30
+ if (rewardLines.length) sections.push(rewardLines.splice(0).join(' \n'))
31
+ }
32
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
27
33
  for (const item of data.line) {
28
34
  switch (item.type) {
29
35
  case MessageType.title:
30
36
  flushBody()
31
- sections.push(renderQQTitle(item.content))
37
+ if (!data.reward) flushRewards()
38
+ if (rewardMessage) bodyLines.push(renderRewardTitle(item.content))
39
+ else sections.push(renderQQTitle(item.content))
32
40
  break
33
41
  case MessageType.line:
42
+ if (item.reward) {
43
+ if (!data.reward) flushBody()
44
+ const reward = renderRewardLine(item.content)
45
+ if (reward) rewardLines.push(reward)
46
+ break
47
+ }
48
+ if (!data.reward) flushRewards()
34
49
  if (item.codeBlock) {
35
50
  flushBody()
36
51
  sections.push(fencedStory(item.content))
37
52
  } else {
38
53
  for (const line of item.content.replace(/\r\n?/g, '\n').split('\n')) {
39
54
  if (!line.trim()) flushBody()
40
- else bodyLines.push(renderInlineCommands(line))
55
+ else bodyLines.push(data.reward ? escapeMarkdown(line) : renderInlineCommands(line))
41
56
  }
42
57
  }
43
58
  break
44
59
  case MessageType.error:
45
60
  flushBody()
46
- sections.push('❌ 出错了!错误原因:' + renderInlineCommands(item.content) + ',请反馈至联系开发者')
61
+ if (!data.reward) flushRewards()
62
+ sections.push('❌ 出错了!错误原因:' + (data.reward ? escapeMarkdown(item.content) : renderInlineCommands(item.content)) + ',请反馈至联系开发者')
47
63
  break
48
64
  }
49
65
  }
50
66
  flushBody()
67
+ flushRewards()
51
68
  return sections.join('\n\n') || ' '
52
69
  }
53
70
 
@@ -2,24 +2,31 @@ import type { Session } from 'koishi'
2
2
  import { MessageType } from '../../shared/face'
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
4
  import { escapeMarkdown, inlineCommandUrl, renderInlineCommands, renderQQTitle, sendQQMarkdown } from './qqPageMessage'
5
+ import { isRewardMessage, renderRewardLine, renderRewardTitle } from './qqRewardMessage'
5
6
 
6
7
  /** Render only explicitly marked transactions; line content never enables this channel. */
7
8
  export function renderQQTransactionMarkdown(data: MsgMessage, showGreeting: boolean): string {
9
+ const rewardMessage = isRewardMessage(data)
8
10
  const sections: string[] = []
9
11
  const bodyLines: string[] = []
10
12
  const actions: string[] = []
13
+ const rewardLines: string[] = []
11
14
  const flushBody = () => {
12
15
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
13
16
  }
14
17
  const flushActions = () => {
15
18
  if (actions.length) sections.push(actions.splice(0).join(' · '))
16
19
  }
17
- const flush = () => { flushBody(); flushActions() }
18
- if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
20
+ const flushRewards = () => {
21
+ if (rewardLines.length) sections.push(rewardLines.splice(0).join(' \n'))
22
+ }
23
+ const flush = () => { flushBody(); flushActions(); flushRewards() }
24
+ if (showGreeting && !data.reward) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
19
25
  for (const item of data.line) {
20
26
  if (item.type === MessageType.title) {
21
27
  flush()
22
- sections.push(renderQQTitle(item.content))
28
+ if (rewardMessage) bodyLines.push(renderRewardTitle(item.content))
29
+ else sections.push(renderQQTitle(item.content))
23
30
  continue
24
31
  }
25
32
  if (item.type === MessageType.error) {
@@ -28,6 +35,14 @@ export function renderQQTransactionMarkdown(data: MsgMessage, showGreeting: bool
28
35
  continue
29
36
  }
30
37
  if (item.type !== MessageType.line) continue
38
+ if (item.reward) {
39
+ flushBody()
40
+ flushActions()
41
+ const reward = renderRewardLine(item.content)
42
+ if (reward) rewardLines.push(reward)
43
+ continue
44
+ }
45
+ flushRewards()
31
46
  // Keep the common trade actions grouped; other bracketed commands render inline.
32
47
  const action = data.transaction && (item.content === '[是]确认交易'
33
48
  ? { command: '是', label: '确认交易' }
@@ -3,10 +3,11 @@ import { AnalysisMsg } from '../../shared/bot/analysisMsg'
3
3
  import { MsgMessage } from '../../shared/bot/MsgMessage'
4
4
  import sessions, { sessionKey } from '../sessions'
5
5
  import handel_base from './handel_base'
6
- import { canSendQQPage, hasInlineCommands, pageCommandLines, sendQQPage } from './qqPageMessage'
6
+ import { canSendQQPage, hasInlineCommands, pageCommandLines, sendQQMarkdown, sendQQPage } from './qqPageMessage'
7
7
  import { sendQQTransaction } from './qqTransactionMessage'
8
8
  import { sendQQCommand } from './qqCommandMessage'
9
9
  import { hasStoryCodeBlock, sendQQStory } from './qqStoryMessage'
10
+ import { isRewardMessage, renderQQRewardMarkdown } from './qqRewardMessage'
10
11
 
11
12
  export default class handelr_text extends handel_base {
12
13
  async start(data: MsgMessage) {
@@ -14,13 +15,15 @@ export default class handelr_text extends handel_base {
14
15
  const session = sessions.get(sessionKey(data.botId, data.userId, data.channelId))
15
16
  const inlineCommands = data.line.some(item => (item.type === MessageType.line || item.type === MessageType.error) && hasInlineCommands(item.content))
16
17
  const story = hasStoryCodeBlock(data)
17
- if ((story || data.commandHelp || data.transaction || data.pageNav || inlineCommands) && canSendQQPage(session, data)) {
18
+ const reward = isRewardMessage(data)
19
+ if ((story || data.commandHelp || data.transaction || data.pageNav || inlineCommands || reward) && canSendQQPage(session, data)) {
18
20
  const sendStartedAt = this.timing ? performance.now() : 0
19
21
  try {
20
22
  if (story) await sendQQStory(session, data)
21
23
  else if (data.commandHelp) await sendQQCommand(session, data)
22
24
  else if (data.transaction) await sendQQTransaction(session, data)
23
25
  else if (data.pageNav) await sendQQPage(session, data)
26
+ else if (reward) await sendQQMarkdown(session, renderQQRewardMarkdown(data, session.event?.channel?.type === 0))
24
27
  else await sendQQCommand(session, data)
25
28
  if (this.timing) {
26
29
  this.timing.delivery = 'qq-markdown'
@@ -20,7 +20,7 @@ export interface ServiceType {
20
20
  }
21
21
 
22
22
  export const serviceProto: ServiceProto<ServiceType> = {
23
- "version": 14,
23
+ "version": 15,
24
24
  "services": [
25
25
  {
26
26
  "id": 0,
@@ -171,6 +171,14 @@ export const serviceProto: ServiceProto<ServiceType> = {
171
171
  },
172
172
  "optional": true
173
173
  },
174
+ {
175
+ "id": 10,
176
+ "name": "reward",
177
+ "type": {
178
+ "type": "Boolean"
179
+ },
180
+ "optional": true
181
+ },
174
182
  {
175
183
  "id": 1,
176
184
  "name": "line",
@@ -915,6 +923,14 @@ export const serviceProto: ServiceProto<ServiceType> = {
915
923
  "type": "Boolean"
916
924
  },
917
925
  "optional": true
926
+ },
927
+ {
928
+ "id": 3,
929
+ "name": "reward",
930
+ "type": {
931
+ "type": "Boolean"
932
+ },
933
+ "optional": true
918
934
  }
919
935
  ]
920
936
  },
@@ -75,6 +75,8 @@ export interface ILineMessage {
75
75
  content: string;
76
76
  /** 原文长故事:支持的平台使用 Markdown 代码块,其余平台按普通文字显示。 */
77
77
  codeBlock?: boolean;
78
+ /** 奖励明细;支持的平台合并为引用区,普通文字仍使用 content。 */
79
+ reward?: boolean;
78
80
  }
79
81
 
80
82
  /** 错误消息项 **/
@@ -109,6 +111,8 @@ export interface IMessage {
109
111
  transaction?: boolean;
110
112
  /** 指令系统与指令提示;客户端可用 Markdown 展示可点击的方框指令。 */
111
113
  commandHelp?: boolean;
114
+ /** 奖励结果卡:标题、描述、奖励;空奖励时也保留同一种布局。 */
115
+ reward?: boolean;
112
116
  /** 消息项列表 **/
113
117
  line: IMessageItem[];
114
118
  }