koishi-plugin-dew-bot 1.0.245 → 1.0.248

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
@@ -1,23 +1,16 @@
1
- # koishi-plugin-dew-bot
2
-
3
- Koishi 插件:把聊天消息交给 dew-v7 Bot 游戏服处理,再把服务器推送的文字发回会话。
4
-
5
- ## 安装
6
-
7
- 在 Koishi 模板项目中把本仓库放到 `external/dew-bot`,然后安装依赖并启用插件。
8
-
9
- ```yaml
10
- plugins:
11
- dew-bot:
12
- 服务器地址: ws://dew-v7-test:3021
13
- ```
14
-
15
- 游戏服需先启动 Bot 进程(默认 WebSocket `3021`)。
16
-
17
- ## 配置
18
-
19
- | 项 | 说明 | 默认 |
20
- | --- | --- | --- |
21
- | 服务器地址 | 游戏服 WebSocket 地址 | `ws://dew-v7-test:3021` |
22
-
23
- 插件会忽略机器人自己发出的消息,避免回显再次上传形成循环。
1
+ # 🌀 DEW · 无限轮回 RPG
2
+
3
+ [![npm](https://img.shields.io/npm/v/koishi-plugin-dew-bot?style=flat-square&color=7c3aed)](https://www.npmjs.com/package/koishi-plugin-dew-bot)
4
+ ![Koishi](https://img.shields.io/badge/Koishi-4.x-6366f1?style=flat-square)
5
+
6
+ > **下一场轮回,主角是你。**
7
+
8
+ **DEW Bot** 是一款基于《无限恐怖》小说改编的 **RPG 机器人**。
9
+
10
+ 踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
11
+
12
+ 无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
13
+
14
+ ---
15
+
16
+ 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
package/lib/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export declare const inject: {
3
3
  optional: string[];
4
4
  };
5
5
  export declare const name = "dew-bot";
6
+ export declare const usage: string;
6
7
  export interface Config {
7
8
  服务器地址: string;
8
9
  }
package/lib/index.js CHANGED
@@ -33,7 +33,8 @@ __export(src_exports, {
33
33
  Config: () => Config,
34
34
  apply: () => apply2,
35
35
  inject: () => inject,
36
- name: () => name
36
+ name: () => name,
37
+ usage: () => usage
37
38
  });
38
39
  module.exports = __toCommonJS(src_exports);
39
40
  var import_koishi2 = require("koishi");
@@ -717,66 +718,91 @@ function escapeMarkdown(value) {
717
718
  return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, "\\$&");
718
719
  }
719
720
  __name(escapeMarkdown, "escapeMarkdown");
720
- function pageCommand(command) {
721
- return command.startsWith("/") ? command : `/${command}`;
721
+ function compactDisplayText(value) {
722
+ 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, "");
722
723
  }
723
- __name(pageCommand, "pageCommand");
724
+ __name(compactDisplayText, "compactDisplayText");
725
+ function compactListLabel(value) {
726
+ const label = compactDisplayText(value);
727
+ return /[📖📚]/u.test(label) ? label.replace(/技能书(?=$|×\d+(?:\.\d+)?(?:||$)||)/g, "") : label;
728
+ }
729
+ __name(compactListLabel, "compactListLabel");
724
730
  function inlineCommandUrl(command) {
725
- const encoded = encodeURIComponent(pageCommand(command)).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
731
+ const encoded = encodeURIComponent(command).replace(/[!'()*]/g, (character) => `%${character.charCodeAt(0).toString(16).toUpperCase()}`);
726
732
  return `mqqapi://aio/inlinecmd?command=${encoded}&enter=true&reply=false`;
727
733
  }
728
734
  __name(inlineCommandUrl, "inlineCommandUrl");
729
735
  function pageCommandLines(nav) {
730
736
  return [
731
- ...nav.previousCommand ? [`上一页:${pageCommand(nav.previousCommand)}`] : [],
732
- ...nav.nextCommand ? [`下一页:${pageCommand(nav.nextCommand)}`] : []
737
+ ...nav.previousCommand ? [nav.previousCommand] : [],
738
+ ...nav.nextCommand ? [nav.nextCommand] : []
733
739
  ];
734
740
  }
735
741
  __name(pageCommandLines, "pageCommandLines");
736
- function renderQQPageMarkdown(data, showGreeting, showCommands) {
742
+ function renderQQPageMarkdown(data, showGreeting) {
737
743
  const nav = data.pageNav;
738
744
  const entryCommands = new Map(nav?.entries?.map((entry) => [entry.lineIndex, entry.viewCommand]) ?? []);
745
+ const listPattern = /^\[(\d+)\]\s*([^\r\n]+)$/;
746
+ const pagePattern = /^第\s*\d+\s*\/\s*\d+\s*页(?:\s*[||]\s*共\s*(\d+)\s*([项条]))?$/;
747
+ const title = data.line.find((item) => item.type === 0 /* title */);
748
+ const summaryLine = data.line.find((item) => item.type === 1 /* line */ && pagePattern.test(item.content));
749
+ const summary = summaryLine && pagePattern.exec(summaryLine.content);
750
+ const listCount = data.line.filter((item) => item.type === 1 /* line */ && listPattern.test(item.content)).length;
751
+ const count = summary?.[1] ?? (nav?.total === 1 ? String(listCount) : void 0);
752
+ const countUnit = summary?.[2] ?? "项";
753
+ const isLeaderboardDirectory = !!nav?.entries?.length && /^(?:🏆)?排行榜$/u.test(compactDisplayText(title?.content ?? ""));
739
754
  const sections = [];
740
755
  const bodyLines = [];
741
- const numberedLines = [];
756
+ const listLines = [];
742
757
  const flushBody = /* @__PURE__ */ __name(() => {
743
758
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(" \n"));
744
759
  }, "flushBody");
745
- const flushNumbered = /* @__PURE__ */ __name(() => {
746
- if (numberedLines.length) sections.push(numberedLines.splice(0).join("\n"));
747
- }, "flushNumbered");
760
+ const flushList = /* @__PURE__ */ __name(() => {
761
+ if (listLines.length) sections.push(listLines.splice(0).join("\n"));
762
+ }, "flushList");
748
763
  const flush = /* @__PURE__ */ __name(() => {
749
- flushNumbered();
764
+ flushList();
750
765
  flushBody();
751
766
  }, "flush");
752
767
  let hasPageSummary = false;
768
+ let hasHelp = false;
769
+ let pageFooter;
753
770
  if (showGreeting) sections.push(escapeMarkdown("Ciallo~(∠・ω< )"));
754
771
  for (const [lineIndex, item] of data.line.entries()) {
755
772
  switch (item.type) {
756
773
  case 0 /* title */:
757
774
  flush();
758
- sections.push(`### ${escapeMarkdown(item.content)}`);
775
+ if (isLeaderboardDirectory) {
776
+ sections.push(`## ${item.content.includes("🏆") ? "" : "🏆"}${escapeMarkdown(compactDisplayText(item.content))}`);
777
+ sections.push(`> 共${nav.entries.length}个榜单·点击榜名查看`);
778
+ } else {
779
+ sections.push(`**${escapeMarkdown(compactDisplayText(item.content))}${count !== void 0 ? `·${count}${countUnit}` : ""}**`);
780
+ }
759
781
  break;
760
782
  case 1 /* line */: {
761
- const numbered = /^\[(\d+)\]\s*([^\r\n]+)$/.exec(item.content);
762
- if (numbered) {
783
+ const listEntry = listPattern.exec(item.content);
784
+ if (listEntry) {
763
785
  flushBody();
764
786
  const command = entryCommands.get(lineIndex);
765
- const label = escapeMarkdown(numbered[2]);
787
+ const label = escapeMarkdown(compactListLabel(listEntry[2]));
766
788
  const name2 = command ? `[${label}](${inlineCommandUrl(command)})` : label;
767
- numberedLines.push(`${numbered[1]}. ${name2}`);
789
+ listLines.push(`${listEntry[1]}. ${name2}`);
790
+ break;
791
+ }
792
+ flushList();
793
+ if (nav && item.content.trim() === "[上一页] [下一页]") {
794
+ flushBody();
768
795
  break;
769
796
  }
770
- flushNumbered();
771
797
  if (/^\[指令\]相关指令$/.test(item.content)) {
772
798
  flushBody();
773
- sections.push(`更多操作:[指令](${inlineCommandUrl("指令")})`);
799
+ hasHelp = true;
774
800
  break;
775
801
  }
776
- const page = /^第\s*\d+\s*\/\s*\d+\s*页(?:\s*[||]\s*共\s*(\d+)\s*([项条]))?$/.exec(item.content);
802
+ const page = pagePattern.exec(item.content);
777
803
  if (page && nav) {
778
804
  flushBody();
779
- sections.push(`**第 ${nav.current} / ${nav.total} 页${page[1] ? ` · 共 ${page[1]} ${page[2]}` : ""}**`);
805
+ pageFooter = `**第${nav.current}/${nav.total}页${!title && page[1] ? `·共${page[1]}${page[2]}` : ""}**`;
780
806
  hasPageSummary = true;
781
807
  break;
782
808
  }
@@ -790,36 +816,19 @@ function renderQQPageMarkdown(data, showGreeting, showCommands) {
790
816
  }
791
817
  }
792
818
  flush();
793
- if (nav && !hasPageSummary) sections.push(`**第 ${nav.current} / ${nav.total} 页**`);
794
- if (showCommands && nav) {
795
- const commands = pageCommandLines(nav);
796
- if (commands.length) sections.push(`**翻页指令**
797
- ${commands.map(escapeMarkdown).join(" \n")}`);
819
+ if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
820
+ pageFooter = `**第${nav.current}/${nav.total}页**`;
821
+ const previousLink = nav?.previousCommand ? `[上一页](${inlineCommandUrl(nav.previousCommand)})` : void 0;
822
+ const nextLink = nav?.nextCommand ? `[下一页](${inlineCommandUrl(nav.nextCommand)})` : void 0;
823
+ const helpLink = hasHelp ? `[指令](${inlineCommandUrl("指令")})` : void 0;
824
+ const footer = pageFooter ? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join("·") : helpLink ? `更多操作:${helpLink}` : void 0;
825
+ if (footer) {
826
+ if (isLeaderboardDirectory) sections.push("***");
827
+ sections.push(footer);
798
828
  }
799
829
  return sections.join("\n\n") || " ";
800
830
  }
801
831
  __name(renderQQPageMarkdown, "renderQQPageMarkdown");
802
- function makeButton(id, label, command, userId, autoEnter) {
803
- return {
804
- id,
805
- render_data: { label, visited_label: label, style: 1 },
806
- action: {
807
- type: 2,
808
- permission: { type: 0, specify_user_ids: [userId] },
809
- data: pageCommand(command),
810
- ...autoEnter ? { enter: true } : {},
811
- unsupport_tips: "当前客户端暂不支持该按钮,请发送上一页或下一页。"
812
- }
813
- };
814
- }
815
- __name(makeButton, "makeButton");
816
- function buttonsFor(nav, userId, autoEnter) {
817
- return [
818
- ...nav.previousCommand ? [makeButton("previous", "上一页", nav.previousCommand, userId, autoEnter)] : [],
819
- ...nav.nextCommand ? [makeButton("next", "下一页", nav.nextCommand, userId, autoEnter)] : []
820
- ];
821
- }
822
- __name(buttonsFor, "buttonsFor");
823
832
  function canSendQQPage(session, data) {
824
833
  if (!session || session.bot.platform !== "qq" || session.bot.userId !== data.botId) return false;
825
834
  if (session.userId !== data.userId || (session.channelId ?? session.userId) !== data.channelId) return false;
@@ -827,23 +836,21 @@ function canSendQQPage(session, data) {
827
836
  return !!internal?.sendMessage && !!internal.sendPrivateMessage;
828
837
  }
829
838
  __name(canSendQQPage, "canSendQQPage");
830
- async function sendQQPage(session, data, withKeyboard) {
839
+ async function sendQQPage(session, data) {
831
840
  const internal = session.bot.internal;
832
841
  const nav = data.pageNav;
833
- const buttons = withKeyboard ? buttonsFor(nav, data.userId, session.isDirect) : [];
834
- const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0, buttons.length === 0);
835
- const hasEntries = !!nav.entries?.length;
842
+ const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0);
843
+ const hasInlineNavigation = !!(nav.previousCommand || nav.nextCommand || nav.entries?.length);
836
844
  const hasInlineHelp = data.line.some((item) => item.type === 1 /* line */ && item.content === "[指令]相关指令");
837
845
  const request = {
838
846
  // With msg_type=2, QQ requires the ordinary content field to be empty.
839
847
  content: "",
840
848
  msg_type: 2,
841
849
  markdown: {
842
- content: !session.isDirect && (buttons.length || hasEntries || hasInlineHelp) ? `${markdown}
850
+ content: !session.isDirect && (hasInlineNavigation || hasInlineHelp) ? `${markdown}
843
851
 
844
852
  点击后如未自动发送,请点发送。` : markdown
845
- },
846
- ...buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}
853
+ }
847
854
  };
848
855
  const age = Date.now() - session.timestamp;
849
856
  if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1e3 - 2e3) {
@@ -868,20 +875,8 @@ var handelr_text = class extends handel_base {
868
875
  const session = sessions_default.get(sessionKey(data.botId, data.userId, data.channelId));
869
876
  if (data.pageNav && canSendQQPage(session, data)) {
870
877
  const sendStartedAt = this.timing ? performance.now() : 0;
871
- if (data.pageNav.previousCommand || data.pageNav.nextCommand) {
872
- try {
873
- await sendQQPage(session, data, true);
874
- if (this.timing) {
875
- this.timing.delivery = "qq-markdown-keyboard";
876
- this.timing.sendMs = performance.now() - sendStartedAt;
877
- }
878
- return;
879
- } catch (error) {
880
- this.ctx.logger.warn("QQ 翻页按钮发送失败,改用 Markdown 文字指令", error instanceof Error ? error.message : error);
881
- }
882
- }
883
878
  try {
884
- await sendQQPage(session, data, false);
879
+ await sendQQPage(session, data);
885
880
  if (this.timing) {
886
881
  this.timing.delivery = "qq-markdown";
887
882
  this.timing.sendMs = performance.now() - sendStartedAt;
@@ -2147,8 +2142,23 @@ __name(apply, "apply");
2147
2142
  // src/index.ts
2148
2143
  var inject = { optional: ["puppeteer"] };
2149
2144
  var name = "dew-bot";
2145
+ var usage = `
2146
+ ## 🌀 DEW · 无限轮回 RPG
2147
+
2148
+ > **下一场轮回,主角是你。**
2149
+
2150
+ **DEW Bot** 是一款基于《无限恐怖》小说改编的 **RPG 机器人**。
2151
+
2152
+ 踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
2153
+
2154
+ 无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
2155
+
2156
+ ---
2157
+
2158
+ 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
2159
+ `;
2150
2160
  var Config = import_koishi2.Schema.object({
2151
- 服务器地址: import_koishi2.Schema.string().default("ws://dew-v7-test:3021").description("游戏服 WebSocket 地址")
2161
+ 服务器地址: import_koishi2.Schema.string().default("ws://dew-v7-test:3021").description("DEW 游戏服的 WebSocket 地址;同机部署可填写 ws://127.0.0.1:3021")
2152
2162
  });
2153
2163
  function apply2(ctx, config) {
2154
2164
  ctx.plugin(apply, config);
@@ -2159,5 +2169,6 @@ __name(apply2, "apply");
2159
2169
  Config,
2160
2170
  apply,
2161
2171
  inject,
2162
- name
2172
+ name,
2173
+ usage
2163
2174
  });
@@ -7,7 +7,7 @@ export interface MessageTiming {
7
7
  renderMs?: number;
8
8
  imageBytes?: number;
9
9
  sendMs?: number;
10
- delivery?: 'image' | 'text' | 'qq-markdown-keyboard' | 'qq-markdown' | 'missing-session';
10
+ delivery?: 'image' | 'text' | 'qq-markdown' | 'missing-session';
11
11
  }
12
12
  export default class handel_base {
13
13
  ctx: Context;
@@ -1,10 +1,9 @@
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
- export declare function pageCommand(command: string): string;
5
4
  export declare function pageCommandLines(nav: PageNavData): string[];
6
- export declare function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, showCommands: boolean): string;
5
+ export declare function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): string;
7
6
  /** Returns false if this is not the matching QQ session or the adapter has no native API. */
8
7
  export declare function canSendQQPage(session: Session | undefined, data: MsgMessage): session is Session;
9
- /** Send a native QQ Markdown page, optionally with auto-sending command buttons. */
10
- export declare function sendQQPage(session: Session, data: MsgMessage, withKeyboard: boolean): Promise<void>;
8
+ /** Send a native QQ Markdown page with inline command links. */
9
+ export declare function sendQQPage(session: Session, data: MsgMessage): Promise<void>;
@@ -108,7 +108,7 @@ export interface IErrorMessage {
108
108
  }
109
109
  /** 第一阶段支持的消息项 **/
110
110
  export type IMessageItem = ITitleMessage | ILineMessage | IErrorMessage;
111
- /** 平台无关的翻页动作;按钮发送普通指令,客户端决定如何呈现。 */
111
+ /** 平台无关的翻页动作;客户端决定如何呈现可点击文字,点击后发送普通指令。 */
112
112
  export interface PageNavData {
113
113
  current: number;
114
114
  total: number;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "koishi-plugin-dew-bot",
3
- "description": "连游戏服,把聊天消息交给服务器处理",
4
- "version": "1.0.245",
3
+ "description": "基于《无限恐怖》小说改编的 RPG 机器人。踏入主神空间,与伙伴迎接轮回试炼,书写属于你的无限冒险。",
4
+ "version": "1.0.248",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -9,11 +9,27 @@
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",
17
+ "homepage": "https://gitee.com/ChuXuanD30/koishi-plugin-dew-bot",
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "git+https://gitee.com/ChuXuanD30/koishi-plugin-dew-bot.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://gitee.com/ChuXuanD30/koishi-plugin-dew-bot/issues"
24
+ },
13
25
  "keywords": [
14
26
  "chatbot",
15
27
  "koishi",
16
- "plugin"
28
+ "plugin",
29
+ "rpg",
30
+ "game",
31
+ "qq",
32
+ "markdown"
17
33
  ],
18
34
  "peerDependencies": {
19
35
  "koishi": "^4.18.0",
@@ -25,7 +41,7 @@
25
41
  },
26
42
  "koishi": {
27
43
  "description": {
28
- "zh": "连游戏服,把聊天消息交给服务器处理"
44
+ "zh": "基于《无限恐怖》小说改编的 RPG 机器人。踏入主神空间,与伙伴迎接轮回试炼,书写属于你的无限冒险。"
29
45
  }
30
46
  }
31
47
  }
package/src/index.ts CHANGED
@@ -5,12 +5,28 @@ export const inject = { optional: ['puppeteer'] }
5
5
 
6
6
  export const name = 'dew-bot'
7
7
 
8
+ export const usage: string = `
9
+ ## 🌀 DEW · 无限轮回 RPG
10
+
11
+ > **下一场轮回,主角是你。**
12
+
13
+ **DEW Bot** 是一款基于《无限恐怖》小说改编的 **RPG 机器人**。
14
+
15
+ 踏入主神空间,成为轮回者。与伙伴迎接未知的试炼,在生死之间积蓄力量,书写属于你的无限冒险。
16
+
17
+ 无论你熟悉原作,还是初次踏入无限流世界,这场轮回,都有你的位置。
18
+
19
+ ---
20
+
21
+ 连接 DEW 游戏服后,在聊天中发送 **帮助**,开启第一场冒险。
22
+ `
23
+
8
24
  export interface Config {
9
25
  服务器地址: string
10
26
  }
11
27
 
12
28
  export const Config: Schema<Config> = Schema.object({
13
- 服务器地址: Schema.string().default('ws://dew-v7-test:3021').description('游戏服 WebSocket 地址'),
29
+ 服务器地址: Schema.string().default('ws://dew-v7-test:3021').description('DEW 游戏服的 WebSocket 地址;同机部署可填写 ws://127.0.0.1:3021'),
14
30
  })
15
31
 
16
32
  export function apply(ctx: Context, config: Config) {
@@ -9,7 +9,7 @@ export interface MessageTiming {
9
9
  renderMs?: number
10
10
  imageBytes?: number
11
11
  sendMs?: number
12
- delivery?: 'image' | 'text' | 'qq-markdown-keyboard' | 'qq-markdown' | 'missing-session'
12
+ delivery?: 'image' | 'text' | 'qq-markdown' | 'missing-session'
13
13
  }
14
14
 
15
15
  export default class handel_base {
@@ -2,23 +2,10 @@ import type { Session } from 'koishi'
2
2
  import { MessageType, type PageNavData } from '../../shared/face'
3
3
  import type { MsgMessage } from '../../shared/bot/MsgMessage'
4
4
 
5
- type Button = {
6
- id: string
7
- render_data: { label: string; visited_label: string; style: number }
8
- action: {
9
- type: 2
10
- permission: { type: 0; specify_user_ids: string[] }
11
- data: string
12
- enter?: true
13
- unsupport_tips: string
14
- }
15
- }
16
-
17
5
  type QQRequest = {
18
6
  content: string
19
7
  msg_type: 2
20
8
  markdown: { content: string }
21
- keyboard?: { content: { rows: Array<{ buttons: Button[] }> } }
22
9
  msg_id?: string
23
10
  msg_seq?: number
24
11
  }
@@ -40,68 +27,105 @@ function escapeMarkdown(value: string): string {
40
27
  return value.replace(/[\\`*_{}\[\]()#+\-.!|>~<&]/g, '\\$&')
41
28
  }
42
29
 
43
- export function pageCommand(command: string): string {
44
- return command.startsWith('/') ? command : `/${command}`
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
+ function compactListLabel(value: string): string {
39
+ const label = compactDisplayText(value)
40
+ // The book icon already identifies skill books. Only shorten the displayed
41
+ // name suffix, including before inventory quantities or market columns.
42
+ return /[📖📚]/u.test(label)
43
+ ? label.replace(/技能书(?=$|×\d+(?:\.\d+)?(?:||$)||)/g, '')
44
+ : label
45
45
  }
46
46
 
47
47
  function inlineCommandUrl(command: string): string {
48
48
  // encodeURIComponent leaves parentheses unescaped, which would close the
49
49
  // surrounding Markdown link if an item ID ever contained one.
50
- const encoded = encodeURIComponent(pageCommand(command)).replace(/[!'()*]/g, character =>
50
+ const encoded = encodeURIComponent(command).replace(/[!'()*]/g, character =>
51
51
  `%${character.charCodeAt(0).toString(16).toUpperCase()}`)
52
52
  return `mqqapi://aio/inlinecmd?command=${encoded}&enter=true&reply=false`
53
53
  }
54
54
 
55
55
  export function pageCommandLines(nav: PageNavData): string[] {
56
56
  return [
57
- ...(nav.previousCommand ? [`上一页:${pageCommand(nav.previousCommand)}`] : []),
58
- ...(nav.nextCommand ? [`下一页:${pageCommand(nav.nextCommand)}`] : []),
57
+ ...(nav.previousCommand ? [nav.previousCommand] : []),
58
+ ...(nav.nextCommand ? [nav.nextCommand] : []),
59
59
  ]
60
60
  }
61
61
 
62
- export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, showCommands: boolean): string {
62
+ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean): string {
63
63
  const nav = data.pageNav
64
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
+ && /^(?:🏆)?排行榜$/u.test(compactDisplayText(title?.content ?? ''))
65
75
  const sections: string[] = []
66
76
  const bodyLines: string[] = []
67
- const numberedLines: string[] = []
77
+ const listLines: string[] = []
68
78
  const flushBody = () => {
69
79
  if (bodyLines.length) sections.push(bodyLines.splice(0).join(' \n'))
70
80
  }
71
- const flushNumbered = () => {
72
- if (numberedLines.length) sections.push(numberedLines.splice(0).join('\n'))
81
+ const flushList = () => {
82
+ if (listLines.length) sections.push(listLines.splice(0).join('\n'))
73
83
  }
74
- const flush = () => { flushNumbered(); flushBody() }
84
+ const flush = () => { flushList(); flushBody() }
75
85
  let hasPageSummary = false
86
+ let hasHelp = false
87
+ let pageFooter: string | undefined
76
88
  if (showGreeting) sections.push(escapeMarkdown('Ciallo~(∠・ω< )'))
77
89
  for (const [lineIndex, item] of data.line.entries()) {
78
90
  switch (item.type) {
79
91
  case MessageType.title:
80
92
  flush()
81
- sections.push(`### ${escapeMarkdown(item.content)}`)
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
+ }
82
99
  break
83
100
  case MessageType.line: {
84
- const numbered = /^\[(\d+)\]\s*([^\r\n]+)$/.exec(item.content)
85
- if (numbered) {
101
+ const listEntry = listPattern.exec(item.content)
102
+ if (listEntry) {
86
103
  flushBody()
87
104
  const command = entryCommands.get(lineIndex)
88
- const label = escapeMarkdown(numbered[2])
105
+ const label = escapeMarkdown(compactListLabel(listEntry[2]))
89
106
  const name = command
90
107
  ? `[${label}](${inlineCommandUrl(command)})`
91
108
  : label
92
- numberedLines.push(`${numbered[1]}. ${name}`)
109
+ // Keep the numeric marker as Markdown syntax; escape only the item text.
110
+ listLines.push(`${listEntry[1]}. ${name}`)
111
+ break
112
+ }
113
+ flushList()
114
+ // Recruitment pages include this legacy plain-text navigation hint.
115
+ // QQ navigation is already rendered as clickable links in the footer.
116
+ if (nav && item.content.trim() === '[上一页] [下一页]') {
117
+ flushBody()
93
118
  break
94
119
  }
95
- flushNumbered()
96
120
  if (/^\[指令\]相关指令$/.test(item.content)) {
97
121
  flushBody()
98
- sections.push(`更多操作:[指令](${inlineCommandUrl('指令')})`)
122
+ hasHelp = true
99
123
  break
100
124
  }
101
- const page = /^第\s*\d+\s*\/\s*\d+\s*页(?:\s*[||]\s*共\s*(\d+)\s*([项条]))?$/.exec(item.content)
125
+ const page = pagePattern.exec(item.content)
102
126
  if (page && nav) {
103
127
  flushBody()
104
- sections.push(`**第 ${nav.current} / ${nav.total} 页${page[1] ? ` · 共 ${page[1]} ${page[2]}` : ''}**`)
128
+ pageFooter = `**第${nav.current}/${nav.total}页${!title && page[1] ? `·共${page[1]}${page[2]}` : ''}**`
105
129
  hasPageSummary = true
106
130
  break
107
131
  }
@@ -115,35 +139,21 @@ export function renderQQPageMarkdown(data: MsgMessage, showGreeting: boolean, sh
115
139
  }
116
140
  }
117
141
  flush()
118
- if (nav && !hasPageSummary) sections.push(`**第 ${nav.current} / ${nav.total} 页**`)
119
- if (showCommands && nav) {
120
- const commands = pageCommandLines(nav)
121
- if (commands.length) sections.push(`**翻页指令** \n${commands.map(escapeMarkdown).join(' \n')}`)
142
+ if (nav && !hasPageSummary && (nav.total > 1 || nav.previousCommand || nav.nextCommand))
143
+ pageFooter = `**第${nav.current}/${nav.total}页**`
144
+ const previousLink = nav?.previousCommand ? `[上一页](${inlineCommandUrl(nav.previousCommand)})` : undefined
145
+ const nextLink = nav?.nextCommand ? `[下一页](${inlineCommandUrl(nav.nextCommand)})` : undefined
146
+ const helpLink = hasHelp ? `[指令](${inlineCommandUrl('指令')})` : undefined
147
+ const footer = pageFooter
148
+ ? [pageFooter, previousLink, nextLink, helpLink].filter(Boolean).join('·')
149
+ : helpLink ? `更多操作:${helpLink}` : undefined
150
+ if (footer) {
151
+ if (isLeaderboardDirectory) sections.push('***')
152
+ sections.push(footer)
122
153
  }
123
154
  return sections.join('\n\n') || ' '
124
155
  }
125
156
 
126
- function makeButton(id: string, label: string, command: string, userId: string, autoEnter: boolean): Button {
127
- return {
128
- id,
129
- render_data: { label, visited_label: label, style: 1 },
130
- action: {
131
- type: 2,
132
- permission: { type: 0, specify_user_ids: [userId] },
133
- data: pageCommand(command),
134
- ...(autoEnter ? { enter: true as const } : {}),
135
- unsupport_tips: '当前客户端暂不支持该按钮,请发送上一页或下一页。',
136
- },
137
- }
138
- }
139
-
140
- function buttonsFor(nav: PageNavData, userId: string, autoEnter: boolean): Button[] {
141
- return [
142
- ...(nav.previousCommand ? [makeButton('previous', '上一页', nav.previousCommand, userId, autoEnter)] : []),
143
- ...(nav.nextCommand ? [makeButton('next', '下一页', nav.nextCommand, userId, autoEnter)] : []),
144
- ]
145
- }
146
-
147
157
  /** Returns false if this is not the matching QQ session or the adapter has no native API. */
148
158
  export function canSendQQPage(session: Session | undefined, data: MsgMessage): session is Session {
149
159
  if (!session || session.bot.platform !== 'qq' || session.bot.userId !== data.botId) return false
@@ -152,27 +162,24 @@ export function canSendQQPage(session: Session | undefined, data: MsgMessage): s
152
162
  return !!internal?.sendMessage && !!internal.sendPrivateMessage
153
163
  }
154
164
 
155
- /** Send a native QQ Markdown page, optionally with auto-sending command buttons. */
156
- export async function sendQQPage(session: Session, data: MsgMessage, withKeyboard: boolean): Promise<void> {
165
+ /** Send a native QQ Markdown page with inline command links. */
166
+ export async function sendQQPage(session: Session, data: MsgMessage): Promise<void> {
157
167
  const internal = (session.bot as typeof session.bot & { internal: QQInternal }).internal
158
168
  const nav = data.pageNav!
159
- const buttons = withKeyboard ? buttonsFor(nav, data.userId, session.isDirect) : []
160
- const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0, buttons.length === 0)
161
- const hasEntries = !!nav.entries?.length
169
+ const markdown = renderQQPageMarkdown(data, session.event?.channel?.type === 0)
170
+ const hasInlineNavigation = !!(nav.previousCommand || nav.nextCommand || nav.entries?.length)
162
171
  const hasInlineHelp = data.line.some(item => item.type === MessageType.line && item.content === '[指令]相关指令')
163
172
  const request: QQRequest = {
164
173
  // With msg_type=2, QQ requires the ordinary content field to be empty.
165
174
  content: '',
166
175
  msg_type: 2,
167
176
  markdown: {
168
- content: !session.isDirect && (buttons.length || hasEntries || hasInlineHelp)
177
+ content: !session.isDirect && (hasInlineNavigation || hasInlineHelp)
169
178
  ? `${markdown}\n\n点击后如未自动发送,请点发送。`
170
179
  : markdown,
171
180
  },
172
- ...(buttons.length ? { keyboard: { content: { rows: [{ buttons }] } } } : {}),
173
181
  }
174
- // Match the QQ adapter's passive-reply sequence, including when a prior
175
- // keyboard attempt failed and this call retries without the keyboard.
182
+ // Match the QQ adapter's passive-reply sequence.
176
183
  const age = Date.now() - session.timestamp
177
184
  if (session.messageId && Number.isFinite(age) && age >= 0 && age < 5 * 60 * 1000 - 2000) {
178
185
  const sequenced = session as Session & { seq?: number }
@@ -10,20 +10,8 @@ export default class handelr_text extends handel_base {
10
10
  const session = sessions.get(sessionKey(data.botId, data.userId, data.channelId))
11
11
  if (data.pageNav && canSendQQPage(session, data)) {
12
12
  const sendStartedAt = this.timing ? performance.now() : 0
13
- if (data.pageNav.previousCommand || data.pageNav.nextCommand) {
14
- try {
15
- await sendQQPage(session, data, true)
16
- if (this.timing) {
17
- this.timing.delivery = 'qq-markdown-keyboard'
18
- this.timing.sendMs = performance.now() - sendStartedAt
19
- }
20
- return
21
- } catch (error) {
22
- this.ctx.logger.warn('QQ 翻页按钮发送失败,改用 Markdown 文字指令', error instanceof Error ? error.message : error)
23
- }
24
- }
25
13
  try {
26
- await sendQQPage(session, data, false)
14
+ await sendQQPage(session, data)
27
15
  if (this.timing) {
28
16
  this.timing.delivery = 'qq-markdown'
29
17
  this.timing.sendMs = performance.now() - sendStartedAt
@@ -83,7 +83,7 @@ export interface IErrorMessage {
83
83
  /** 第一阶段支持的消息项 **/
84
84
  export type IMessageItem = ITitleMessage | ILineMessage | IErrorMessage;
85
85
 
86
- /** 平台无关的翻页动作;按钮发送普通指令,客户端决定如何呈现。 */
86
+ /** 平台无关的翻页动作;客户端决定如何呈现可点击文字,点击后发送普通指令。 */
87
87
  export interface PageNavData {
88
88
  current: number;
89
89
  total: number;