koishi-plugin-dew-bot 1.0.244 → 1.0.246

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
@@ -21,3 +21,33 @@ plugins:
21
21
  | 服务器地址 | 游戏服 WebSocket 地址 | `ws://dew-v7-test:3021` |
22
22
 
23
23
  插件会忽略机器人自己发出的消息,避免回显再次上传形成循环。
24
+
25
+ ## 发布到 npm
26
+
27
+ 在 `bot/external/dew-bot` 目录执行:
28
+
29
+ ```bash
30
+ npm run release
31
+ ```
32
+
33
+ 脚本会先从 `bot` 工作区执行 `npm run build -- dew-bot`,构建成功后运行 `npm version patch --no-git-tag-version --workspaces=false`,再以 `public` 权限和 `latest` 标签发布到 `https://registry.npmjs.org`。例如 `1.0.245` 会提升为 `1.0.246`,不会创建 Git 提交或标签。发布前需已安装工作区依赖,并登录有该包发布权限的 npm 账号:
34
+
35
+ ```bash
36
+ npm login --registry=https://registry.npmjs.org
37
+ ```
38
+
39
+ 仅构建并检查包内容,执行:
40
+
41
+ ```bash
42
+ npm run release:check
43
+ ```
44
+
45
+ 这相当于 `npm run release -- --dry-run`,会真实构建并执行 `npm pack --dry-run`,不会提升版本号、生成压缩包或发布到 npm。
46
+
47
+ 构建失败时脚本会停止,版本不递增。若递增后发布失败,本地保留新版本,脚本会以非零退出码结束并打印当前版本及重试命令;在插件目录确认该版本的发布状态后,可重试同一版本:
48
+
49
+ ```bash
50
+ npm publish --workspaces=false --access=public --tag=latest --registry=https://registry.npmjs.org
51
+ ```
52
+
53
+ 重试发布时不要再次运行 `npm run release`,否则会再次递增版本。
package/lib/index.js CHANGED
@@ -721,6 +721,11 @@ function pageCommand(command) {
721
721
  return command.startsWith("/") ? command : `/${command}`;
722
722
  }
723
723
  __name(pageCommand, "pageCommand");
724
+ function inlineCommandUrl(command) {
725
+ const encoded = encodeURIComponent(pageCommand(command)).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
726
+ return `mqqapi://aio/inlinecmd?command=${encoded}&enter=true&reply=false`;
727
+ }
728
+ __name(inlineCommandUrl, "inlineCommandUrl");
724
729
  function pageCommandLines(nav) {
725
730
  return [
726
731
  ...nav.previousCommand ? [`上一页:${pageCommand(nav.previousCommand)}`] : [],
@@ -729,23 +734,90 @@ function pageCommandLines(nav) {
729
734
  }
730
735
  __name(pageCommandLines, "pageCommandLines");
731
736
  function renderQQPageMarkdown(data, showGreeting, showCommands) {
737
+ const nav = data.pageNav;
738
+ const entryCommands = new Map(nav?.entries?.map((entry) => [entry.lineIndex, entry.viewCommand]) ?? []);
739
+ const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/;
740
+ const pagePattern = /^第\s*\d+\s*\/\s*\d+\s*页(?:\s*[||]\s*共\s*(\d+)\s*([项条]))?$/;
741
+ const title = data.line.find((item) => item.type === 0 /* title */);
742
+ const summaryLine = data.line.find((item) => item.type === 1 /* line */ && pagePattern.test(item.content));
743
+ const summary = summaryLine && pagePattern.exec(summaryLine.content);
744
+ const listCount = data.line.filter((item) => item.type === 1 /* line */ && listPattern.test(item.content)).length;
745
+ const count = summary?.[1] ?? (nav?.total === 1 ? String(listCount) : void 0);
746
+ const countUnit = summary?.[2] ?? "项";
747
+ const isLeaderboardDirectory = !!nav?.entries?.length && nav.entries.every((entry) => /^排行榜查看\s/.test(entry.viewCommand));
732
748
  const sections = [];
749
+ const bodyLines = [];
750
+ const listLines = [];
751
+ const flushBody = /* @__PURE__ */ __name(() => {
752
+ if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
753
+ }, "flushBody");
754
+ const flushList = /* @__PURE__ */ __name(() => {
755
+ if (listLines.length) sections.push(listLines.splice(0).join(" \n"));
756
+ }, "flushList");
757
+ const flush = /* @__PURE__ */ __name(() => {
758
+ flushList();
759
+ flushBody();
760
+ }, "flush");
761
+ let hasPageSummary = false;
762
+ let hasHelp = false;
763
+ let pageFooter;
733
764
  if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
734
- for (const item of data.line) {
765
+ for (const [lineIndex, item] of data.line.entries()) {
735
766
  switch (item.type) {
736
767
  case 0 /* title */:
737
- sections.push(`### ${escapeMarkdown(item.content)}`);
768
+ flush();
769
+ if (isLeaderboardDirectory) {
770
+ sections.push(`## ${item.content.includes("🏆") ? "" : "🏆 "}${escapeMarkdown(item.content)}`);
771
+ sections.push(`> 共 ${nav.entries.length} 个榜单 · 点击榜名查看`);
772
+ } else {
773
+ sections.push(`**${escapeMarkdown(item.content)}${count !== void 0 ? ` · ${count} ${countUnit}` : ""}**`);
774
+ }
738
775
  break;
739
- case 1 /* line */:
740
- sections.push(escapeMarkdown(item.content));
776
+ case 1 /* line */: {
777
+ const listEntry = listPattern.exec(item.content);
778
+ if (listEntry) {
779
+ flushBody();
780
+ const command = entryCommands.get(lineIndex);
781
+ const label = escapeMarkdown(listEntry[2]);
782
+ const name2 = command ? `[${label}](${inlineCommandUrl(command)})` : label;
783
+ listLines.push(`${escapeMarkdown(`[${listEntry[1]}]`)} ${name2}`);
784
+ break;
785
+ }
786
+ flushList();
787
+ if (/^\[指令\]相关指令$/.test(item.content)) {
788
+ flushBody();
789
+ hasHelp = true;
790
+ break;
791
+ }
792
+ const page = pagePattern.exec(item.content);
793
+ if (page && nav) {
794
+ flushBody();
795
+ pageFooter = `**第 ${nav.current} / ${nav.total} 页${!title && page[1] ? ` · 共 ${page[1]} ${page[2]}` : ""}**`;
796
+ hasPageSummary = true;
797
+ break;
798
+ }
799
+ bodyLines.push(escapeMarkdown(item.content));
741
800
  break;
801
+ }
742
802
  case 2 /* error */:
803
+ flush();
743
804
  sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`);
744
805
  break;
745
806
  }
746
807
  }
747
- if (showCommands && data.pageNav) {
748
- sections.push(...pageCommandLines(data.pageNav).map(escapeMarkdown));
808
+ flush();
809
+ if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
810
+ pageFooter = `**第 ${nav.current} / ${nav.total} 页**`;
811
+ const helpLink = hasHelp ? `[指令](${inlineCommandUrl("指令")})` : void 0;
812
+ const footer = pageFooter ? [pageFooter, helpLink].filter(Boolean).join(" · ") : helpLink ? `更多操作:${helpLink}` : void 0;
813
+ if (footer) {
814
+ if (isLeaderboardDirectory) sections.push("***");
815
+ sections.push(footer);
816
+ }
817
+ if (showCommands && nav) {
818
+ const commands = pageCommandLines(nav);
819
+ if (commands.length) sections.push(`**翻页指令**
820
+ ${commands.map(escapeMarkdown).join(" \n")}`);
749
821
  }
750
822
  return sections.join("\n\n") || " ";
751
823
  }
@@ -783,14 +855,16 @@ async function sendQQPage(session, data, withKeyboard) {
783
855
  const nav = data.pageNav;
784
856
  const buttons = withKeyboard ? buttonsFor(nav, data.userId, session.isDirect) : [];
785
857
  const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0, buttons.length === 0);
858
+ const hasEntries = !!nav.entries?.length;
859
+ const hasInlineHelp = data.line.some((item) => item.type === 1 /* line */ && item.content === "[指令]相关指令");
786
860
  const request = {
787
861
  // With msg_type=2, QQ requires the ordinary content field to be empty.
788
862
  content: "",
789
863
  msg_type: 2,
790
864
  markdown: {
791
- content: buttons.length && !session.isDirect ? `${markdown}
865
+ content: !session.isDirect && (buttons.length || hasEntries || hasInlineHelp) ? `${markdown}
792
866
 
793
- 群聊中点击按钮后,请发送已填入的指令。` : markdown
867
+ 点击后如未自动发送,请点发送。` : markdown
794
868
  },
795
869
  ...buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}
796
870
  };
@@ -912,7 +986,7 @@ var import_tsrpc = require("tsrpc");
912
986
 
913
987
  // src/shared/bot/serviceProto.ts
914
988
  var serviceProto = {
915
- "version": 9,
989
+ "version": 10,
916
990
  "services": [
917
991
  {
918
992
  "id": 0,
@@ -1665,6 +1739,33 @@ var serviceProto = {
1665
1739
  "type": "String"
1666
1740
  },
1667
1741
  "optional": true
1742
+ },
1743
+ {
1744
+ "id": 4,
1745
+ "name": "entries",
1746
+ "type": {
1747
+ "type": "Array",
1748
+ "elementType": {
1749
+ "type": "Interface",
1750
+ "properties": [
1751
+ {
1752
+ "id": 0,
1753
+ "name": "lineIndex",
1754
+ "type": {
1755
+ "type": "Number"
1756
+ }
1757
+ },
1758
+ {
1759
+ "id": 1,
1760
+ "name": "viewCommand",
1761
+ "type": {
1762
+ "type": "String"
1763
+ }
1764
+ }
1765
+ ]
1766
+ }
1767
+ },
1768
+ "optional": true
1668
1769
  }
1669
1770
  ]
1670
1771
  },
@@ -114,6 +114,11 @@ export interface PageNavData {
114
114
  total: number;
115
115
  previousCommand?: string;
116
116
  nextCommand?: string;
117
+ /** 可点击的列表行;lineIndex 是 IMessage.line 的零起始位置。 */
118
+ entries?: Array<{
119
+ lineIndex: number;
120
+ viewCommand: string;
121
+ }>;
117
122
  }
118
123
  /** 下发给客户端的消息主体 **/
119
124
  export interface IMessage {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-dew-bot",
3
3
  "description": "连游戏服,把聊天消息交给服务器处理",
4
- "version": "1.0.244",
4
+ "version": "1.0.246",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -9,6 +9,10 @@
9
9
  "src",
10
10
  "assets"
11
11
  ],
12
+ "scripts": {
13
+ "release": "node scripts/release.cjs",
14
+ "release:check": "node scripts/release.cjs --dry-run"
15
+ },
12
16
  "license": "MIT",
13
17
  "keywords": [
14
18
  "chatbot",
@@ -44,6 +44,14 @@ export function pageCommand(command: string): string {
44
44
  return command.startsWith('/') ? command : `/${command}`
45
45
  }
46
46
 
47
+ function inlineCommandUrl(command: string): string {
48
+ // encodeURIComponent leaves parentheses unescaped, which would close the
49
+ // surrounding Markdown link if an item ID ever contained one.
50
+ const encoded = encodeURIComponent(pageCommand(command)).replace(/[!'()*]/g, character =>
51
+ `%${character.charCodeAt(0).toString(16).toUpperCase()}`)
52
+ return `mqqapi://aio/inlinecmd?command=${encoded}&enter=true&reply=false`
53
+ }
54
+
47
55
  export function pageCommandLines(nav: PageNavData): string[] {
48
56
  return [
49
57
  ...(nav.previousCommand ? [`上一页:${pageCommand(nav.previousCommand)}`] : []),
@@ -52,23 +60,91 @@ export function pageCommandLines(nav: PageNavData): string[] {
52
60
  }
53
61
 
54
62
  export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, showCommands: boolean): string {
63
+ const nav = data.pageNav
64
+ const entryCommands = new Map(nav?.entries?.map(entry => [entry.lineIndex, entry.viewCommand]) ?? [])
65
+ const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/
66
+ const pagePattern = /^第\s*\d+\s*\/\s*\d+\s*页(?:\s*[||]\s*共\s*(\d+)\s*([项条]))?$/
67
+ 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 ? String(listCount) : undefined)
72
+ const countUnit = summary?.[2] ?? '项'
73
+ const isLeaderboardDirectory = !!nav?.entries?.length
74
+ && nav.entries.every(entry => /^排行榜查看\s/.test(entry.viewCommand))
55
75
  const sections: string[] = []
76
+ const bodyLines: string[] = []
77
+ const listLines: string[] = []
78
+ const flushBody = () => {
79
+ if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
80
+ }
81
+ const flushList = () => {
82
+ if (listLines.length) sections.push(listLines.splice(0).join(' \n'))
83
+ }
84
+ const flush = () => { flushList(); flushBody() }
85
+ let hasPageSummary = false
86
+ let hasHelp = false
87
+ let pageFooter: string | undefined
56
88
  if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
57
- for (const item of data.line) {
89
+ for (const [lineIndex, item] of data.line.entries()) {
58
90
  switch (item.type) {
59
91
  case MessageType.title:
60
- sections.push(`### ${escapeMarkdown(item.content)}`)
92
+ flush()
93
+ if (isLeaderboardDirectory) {
94
+ sections.push(`## ${item.content.includes('🏆') ? '' : '🏆 '}${escapeMarkdown(item.content)}`)
95
+ sections.push(`> 共 ${nav!.entries!.length} 个榜单 · 点击榜名查看`)
96
+ } else {
97
+ sections.push(`**${escapeMarkdown(item.content)}${count !== undefined ? ` · ${count} ${countUnit}` : ''}**`)
98
+ }
61
99
  break
62
- case MessageType.line:
63
- sections.push(escapeMarkdown(item.content))
100
+ case MessageType.line: {
101
+ const listEntry = listPattern.exec(item.content)
102
+ if (listEntry) {
103
+ flushBody()
104
+ const command = entryCommands.get(lineIndex)
105
+ const label = escapeMarkdown(listEntry[2])
106
+ const name = command
107
+ ? `[${label}](${inlineCommandUrl(command)})`
108
+ : label
109
+ listLines.push(`${escapeMarkdown(`[${listEntry[1]}]`)} ${name}`)
110
+ break
111
+ }
112
+ flushList()
113
+ if (/^\[指令\]相关指令$/.test(item.content)) {
114
+ flushBody()
115
+ hasHelp = true
116
+ break
117
+ }
118
+ const page = pagePattern.exec(item.content)
119
+ if (page && nav) {
120
+ flushBody()
121
+ pageFooter = `**第 ${nav.current} / ${nav.total} 页${!title && page[1] ? ` · 共 ${page[1]} ${page[2]}` : ''}**`
122
+ hasPageSummary = true
123
+ break
124
+ }
125
+ bodyLines.push(escapeMarkdown(item.content))
64
126
  break
127
+ }
65
128
  case MessageType.error:
129
+ flush()
66
130
  sections.push(`❌ 出错了!错误原因:${escapeMarkdown(item.content)},请反馈至联系开发者`)
67
131
  break
68
132
  }
69
133
  }
70
- if (showCommands && data.pageNav) {
71
- sections.push(...pageCommandLines(data.pageNav).map(escapeMarkdown))
134
+ flush()
135
+ if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
136
+ pageFooter = `**第 ${nav.current} / ${nav.total} 页**`
137
+ const helpLink = hasHelp ? `[指令](${inlineCommandUrl('指令')})` : undefined
138
+ const footer = pageFooter
139
+ ? [pageFooter, helpLink].filter(Boolean).join(' · ')
140
+ : helpLink ? `更多操作:${helpLink}` : undefined
141
+ if (footer) {
142
+ if (isLeaderboardDirectory) sections.push('***')
143
+ sections.push(footer)
144
+ }
145
+ if (showCommands && nav) {
146
+ const commands = pageCommandLines(nav)
147
+ if (commands.length) sections.push(`**翻页指令** \n${commands.map(escapeMarkdown).join(' \n')}`)
72
148
  }
73
149
  return sections.join('\n\n') || ' '
74
150
  }
@@ -108,13 +184,15 @@ export async function sendQQPage(session: Session, data: MsgMessage, withKeyboar
108
184
  const nav = data.pageNav!
109
185
  const buttons = withKeyboard ? buttonsFor(nav, data.userId, session.isDirect) : []
110
186
  const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0, buttons.length === 0)
187
+ const hasEntries = !!nav.entries?.length
188
+ const hasInlineHelp = data.line.some(item => item.type === MessageType.line && item.content === '[指令]相关指令')
111
189
  const request: QQRequest = {
112
190
  // With msg_type=2, QQ requires the ordinary content field to be empty.
113
191
  content: '',
114
192
  msg_type: 2,
115
193
  markdown: {
116
- content: buttons.length && !session.isDirect
117
- ? `${markdown}\n\n群聊中点击按钮后,请发送已填入的指令。`
194
+ content: !session.isDirect && (buttons.length || hasEntries || hasInlineHelp)
195
+ ? `${markdown}\n\n点击后如未自动发送,请点发送。`
118
196
  : markdown,
119
197
  },
120
198
  ...(buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}),
@@ -20,7 +20,7 @@ export interface ServiceType {
20
20
  }
21
21
 
22
22
  export const serviceProto: ServiceProto<ServiceType> = {
23
- "version": 9,
23
+ "version": 10,
24
24
  "services": [
25
25
  {
26
26
  "id": 0,
@@ -773,6 +773,33 @@ export const serviceProto: ServiceProto<ServiceType> = {
773
773
  "type": "String"
774
774
  },
775
775
  "optional": true
776
+ },
777
+ {
778
+ "id": 4,
779
+ "name": "entries",
780
+ "type": {
781
+ "type": "Array",
782
+ "elementType": {
783
+ "type": "Interface",
784
+ "properties": [
785
+ {
786
+ "id": 0,
787
+ "name": "lineIndex",
788
+ "type": {
789
+ "type": "Number"
790
+ }
791
+ },
792
+ {
793
+ "id": 1,
794
+ "name": "viewCommand",
795
+ "type": {
796
+ "type": "String"
797
+ }
798
+ }
799
+ ]
800
+ }
801
+ },
802
+ "optional": true
776
803
  }
777
804
  ]
778
805
  },
@@ -89,6 +89,8 @@ export interface PageNavData {
89
89
  total: number;
90
90
  previousCommand?: string;
91
91
  nextCommand?: string;
92
+ /** 可点击的列表行;lineIndex 是 IMessage.line 的零起始位置。 */
93
+ entries?: Array<{ lineIndex: number; viewCommand: string }>;
92
94
  }
93
95
 
94
96
  /** 下发给客户端的消息主体 **/